Appearance
Authenticating Users
Overview
How to authenticate Canva 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.
Types of authentication
Apps can handle users with two forms of authentication: frictionless, or manual.
Frictionless authentication (preferred)
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. This ensures that user data or credentials are automatically remembered to help users retain data, even without them passing through a login screen.
For more information on adding frictionless authentication to your app, see Frictionless authentication.
OAuth integration
If your app requires access to resources on a third-party platform (e.g. Google Drive), you can use OAuth to simplify the authorization process for your users. This method allows users to securely grant your app the necessary permissions with a third party service without exposing user credentials.
For more information, see OAuth integration.
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. Your app can use manual authentication, but the way you handle users and user data can affect your apps inclusion in the user adoption program and your app's positioning in the Apps Marketplace. Consider using frictionless authentication, and make sure to follow our authentication guidelines.
For more information on adding manual authentication to your app, see Manual authentication.
Authentication workflows
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.
Handle existing or returning users
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.
Support a free trial without authentication
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.
Free and freemium
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).
Alternative authentication experiences
Most apps should use one of the standard authentication patterns described above. However, in rare cases where your app’s requirements can't be met using frictionless authentication, OAuth, or manual authentication, you may need to consider an alternative approach.
For guidance on how to handle these exceptional scenarios securely, see Alternative authentication experiences.
API reference
auth.getCanvaUserTokenauth.requestAuthentication
Frictionless Auth
Seamlessly authenticate users.
Frictionless authentication is when you use the auth.getCanvaUserToken method to retain a user's data without them having to sign-in through 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.
User tokens
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.
High-level workflow
The workflow looks like the following:
- The user interacts with your app. Canva assigns a unique user ID to the user.
- 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.
- 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.
Prerequisites
Before you begin, review the Authentication design guidelines.
Step 1: Get a JWT for the current user
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:
Import the
authnamespace from the@canva/userpackage:tsimport { auth } from "@canva/user";Call the
getCanvaUserTokenmethod:tsconst token = await auth.getCanvaUserToken();This method returns a JWT as a string.
Step 2: Verify the JWT in your backend
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:
ts
const response = await fetch("http://localhost:3001/my/api/endpoint", {
headers: {
Authorization: `Bearer ${token}`,
},
});When sending the request, include an Authorization header that contains the word Bearer and the JWT, separated by a space.
To verify the JWT, you can either use the @canva/app-middleware package (recommended), or manually implement your own verification.
Recommended: Using @canva/app-middleware
For Node.js backends, we recommend using the @canva/app-middleware package to automatically verify JWTs:
Express.js middleware
typescript
import express from "express";
import { user } from "@canva/app-middleware/express";
const app = express();
// Apply middleware to verify all requests
app.use("/my/api", user.verifyToken({ appId: process.env.CANVA_APP_ID }));
app.post("/my/api/endpoint", (req, res) => {
// Access verified user information
const { userId, brandId } = req.canva.user;
// Your application logic here
res.sendStatus(200);
});
app.listen(process.env.PORT || 3000);For more information, see user.verifyToken.
Framework-agnostic approach
For other Node.js environments:
Alternative: Manual verification
For non-Node.js backends, follow the steps in JSON Web Tokens to manually verify the JWT. If the JWT is valid, you will have an object that contains the ID of the user and their team.
Step 3: Associate data with the user
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 to get the ID of the user and their team.
Step 4: Handle disconnections
Tip: Review Canva's design guidelines for handling app disconnections and reconnections.
After a user disconnects (uninstalls) an app, you should remove any persisted data associated with their account. This ensures that if they reinstall the app later, they'll start with a fresh state.
To handle disconnections, you must configure a webhook URL in the Developer Portal. This URL will receive a POST request from Canva when a user disconnects your app.
Configure the user uninstall webhook URL
- Log in to the Developer Portal.
- Navigate to your app's settings.
- Go to the Webhooks section.
- Add a new webhook URL for User uninstalls.
Handle the webhook and remove data
When a user disconnects your app, Canva sends a POST request to your webhook URL. The request includes an Authorization header containing a JWT that identifies the user:
Authorization: Bearer <jwt>When your backend receives the webhook:
- Extract the JWT from the
Authorizationheader. - Verify the JWT to get the ID of the user and their team.
- Use the user and team IDs to find and remove any persisted data associated with the user, such as:
- Usage history
- Saved preferences
- Generated content
- Any other user-specific data
Respond to the webhook with a 200 status code and the following object to acknowledge receipt:
json
{
"type": "SUCCESS"
}Example webhook implementation
For Node.js backends using Express.js:
typescript
import express from "express";
import { user } from "@canva/app-middleware/express";
const app = express();
// Apply middleware to verify the webhook JWT
app.use("/webhooks", user.verifyToken({ appId: process.env.CANVA_APP_ID }));
app.post("/webhooks/user-uninstall", async (req, res) => {
// Get verified user information from the JWT
const { userId, brandId } = req.canva.user;
// Remove all data associated with this user
await removeUserData(userId, brandId);
// Acknowledge receipt
res.json({ type: "SUCCESS" });
});
app.listen(3000);For more information, see user.verifyToken.
Confirm the disconnection flow works
To confirm that the disconnection flow is working:
- Navigate through the app and create some data.
- Disconnect (uninstall) the app.
- Reconnect (install) the app.
You should see that all previously created data is gone, and the user starts with a fresh state. If the data persists, double-check your disconnection logic.
API reference
auth.getCanvaUserTokenauth.requestAuthentication
Manual Auth
How to set up authentication with a third-party platform.
Warning: Manual authentication was removed in Apps SDK v2. You should use one of the other authentication strategies, such as frictionless authentication or OAuth.
Overview
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:
- D-ID Ai Presenters
- Soundraw
For more examples, see the Apps Marketplace.
Lifecycle
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.
Prerequisites
Before you begin, review the Authentication design guidelines.
Step 1: Enable authentication
By default, authentication is disabled.
To enable authentication:
- Log in to the Developer Portal.
- Navigate to an app via the Your apps page.
- Click Add authentication.
- 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.
Step 2: Start an authentication flow
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:
Import the
authnamespace from the@canva/userpackage:tsimport { auth } from "@canva/user";Call the
requestAuthenticationmethod:tsconst result = await auth.requestAuthentication();
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:
Then the complete URL would be:
This URL doesn't exist yet though, so we'll set it up in the next step.
Exposing the backend
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.
Warning: There are inherent risks in using tools such as ngrok. Be careful about what you expose to the public internet and shut-down the ngrok tunnel as soon as you're done using it.
Step 3: Set up the /configuration/start endpoint
In the app's backend, set up an endpoint that handle GET requests sent to /configuration/start:
ts
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}`);
});This endpoint must:
- Extract the
stateparameter 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
Warning: Don't attempt to verify the HTTP request that's sent to this endpoint. It does not have access to the necessary parameters that make verification possible.
Extracting the state parameter
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:
ts
const { state } = req.query;Generating a nonce
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:
ts
import * as crypto from "crypto";
const nonce = crypto.randomUUID();Warning: Not all UUIDs are cryptographically secure, such as ones generated with the uuid package.
Setting a cookie
After generating a nonce, set a cookie that contains the nonce and an expiry time.
The cookie should be:
- Secure
- HTTP-only
- Signed
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:
- Go
- Fiber
- Node.js
- Express.js
- Remix
- PHP
- Laravel
- Python
- Django
- Flask
- Ruby
- Ruby on Rails
The following code sample demonstrates how to set cookies with Express.js and cookieParser:
ts
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,
});
});It's worth nothing that:
- To sign a cookie, you need a cryptographically secure secret, such as one generated by the
randomUUIDmethod. 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
maxAgeproperty is set in milliseconds, but theMax-Ageattribute value must be set in seconds.
Redirecting back to Canva
After setting the cookie, redirect the user to the following URL with a 302 redirect:
You'll need to:
- Replace
<state>with thestatequery parameter that Canva included with the request. - Replace
<nonce>with the nonce that was stored in the cookie.
For example:
ts
// 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()}`
);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.
Step 4: Set up a Redirect URL
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_tokennoncestate
Your app's backend can use these parameters to securely authenticate a user.
Note: You may notice some other parameters appended to the Redirect URL. These parameters are deprecated and should be ignored.
Validating the nonce
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:
Get the
noncequery parameter:tsconst nonceQuery = req.query.nonce;Get the cookie that contains the nonce and its expiry time:
tsconst nonceWithExpiryCookie = req.signedCookies.nonceWithExpiry;Clear the cookie:
tsres.clearCookie("nonceWithExpiry");Parse the value of the cookie:
tstry { const [nonceCookie, nonceExpiry] = JSON.parse(nonceWithExpiryCookie); } catch (e) { // TODO: Handle errors }(This code uses a try/catch block to account for the possibility of JSON parsing exceptions.)
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:
tsif ( 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 }
Handling invalid nonces
If the nonce validation fails, redirect the user to the following URL with a 302 redirect:
You'll need to:
- Replace
<state>with thestatequery 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:
ts
// 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()}`
);
}We also recommend logging a security alert, as invalid nonces suggest a potential threat.
Authenticating the user
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:
- Grab the
canva_user_tokenquery parameter that Canva includes with the request. This parameter is a JSON Web Token (JWT) that is unique to the user. - Verify the JWT to get the ID of the user and their team.
- 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.
Step 5: End the authentication flow
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:
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:
Be sure to:
- Replace
<state>with the value of thestatetoken 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:
ts
const params = new URLSearchParams({
success: "true",
state: req.query.state,
});
response.redirect(
302,
`https://www.canva.com/apps/configured?${params.toString()}`
);If you're performing the redirect via the app's frontend, use the window.location.replace method:
ts
const params = new URLSearchParams({
success: "true",
state: req.query.state,
});
window.location.replace(
`https://www.canva.com/apps/configured?${params.toString()}`
);Step 6: Handle the authentication result
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:
ts
const result = await auth.requestAuthentication();
console.log(result.status);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.
Step 7: Send authenticated requests
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:
ts
const token = await auth.getCanvaUserToken();Then send an HTTP request to the app's backend with the JWT in the Authorization header:
ts
const response = await fetch("https://localhost:3001/authenticated-endpoint", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
});When the backend receives the request:
- Extract the JWT from the
Authorizationheader. - Verify the JWT to get the ID of the user and their team.
- 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.
Step 8: Handle disconnections
Tip: Review Canva's design guidelines for handling app disconnections and reconnections.
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.
Warning: The /configuration/delete endpoint must not redirect to a different endpoint. Even a redirect that just appends a / to the end of the endpoint will cause the request to fail.
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:
- Extract the JWT from the
Authorizationheader. - Verify the JWT to get the ID of the user and their team.
- Use the IDs to find the user in the app's backend.
- Remove any connection between the user and Canva.
When the disconnection is complete, respond with a 200 status code and the following object:
json
{
"type": "SUCCESS"
}To confirm that the disconnection flow is working:
- Navigate through the authentication flow.
- Disconnect (uninstall) the app.
- 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.
API reference
auth.getCanvaUserTokenauth.requestAuthentication
OAuth
Let your users access resources in an external platform.
Introduction
If your app requires access to resources on a third-party platform (e.g. Google Drive), you can use OAuth to simplify the authorization process for your users. This method allows users to securely grant your app the necessary permissions with a third party service without exposing user credentials.
Canva enhances this process by handling the OAuth flow and the management of access and refresh tokens. This means that Canva does the heavy lifting in ensuring that your app maintains continuous and security-hardened access to the third-party resources it needs, streamlining the user experience and reducing the development burden on your team. Canva currently supports the authorization code flow, with and without Proof-of Key Code Exchange (PKCE).
The authentication API makes it easier to adopt the industry-standard OAuth 2, because Canva's servers are responsible for interacting directly with your chosen Identity Provider (IdP) — including exchanging authorization codes for tokens.
To learn more about the OAuth concepts and terminology, see Key terms.
Overview
To implement OAuth in your app, just do the following:
- Configure your chosen identity provider (such as Google or Facebook or your own backend that supports OAuth).
- Copy the configuration details into Canva's Developer Portal.
- Call the API method
oauth.requestAuthorizationmethod in your app. - Canva retrieves and stores refresh tokens and access tokens.
- Use the API method
getAccessTokento retrieve the latest access token. - Use
fetchwith the authorization headerBearer <access token>
With this approach, you won't need an additional server to store client IDs, client secrets, access or refresh tokens. You also don't need to handle the token expiry, because Canva does all of this for you.
For more information about the OAuth standard, see RFC6749.
OAuth workflow: Authentication capability
The following diagram demonstrates the OAuth workflow with the Canva capability.
- User clicks the login button.
- The authorization flow begins.
- Canva generates the authorization URL, based on the configuration you provided in the developer portal. If your IdP supports PKCE, then Canva also generates a code challenge.
- A popup window opens for the authorization URL.
- User logs into the IdP and the popup window is redirected back to Canva by the IdP, using the redirect URI shown in the Developer Portal. This redirection must include the following:
- The state that Canva originally provided in the authorization URL.
- The authorization code generated by the IdP.
- Canva receives the authorization code.
- Canva retrieves the access and optional refresh tokens from the IdP, and if PKCE is enabled it also uses the code verifier. Some IdPs (such as Google) require additional configuration for the response to include the refresh token; if the refresh token isn't provided then the user must re-login to the app once the access token expires.
- Canva stores the access and refresh tokens.
- The popup window closes and the authorization flow concludes.
- Your app code requests a token using the
getAccessTokenauth API, and Canva returns the access token to the application.
- Your app then uses the access token to retrieve resources from the resource server.
- If a refresh token was provided, then Canva will automatically refresh the token when the access token expires or
forceRefreshis called.
- If a refresh token was provided, then Canva will automatically refresh the token when the access token expires or
Token endpoint request format
Canva calls your IdP's Token exchange URL directly. The request is a standard application/x-www-form-urlencoded HTTPS POST as defined in RFC 6749 §4.1.3. There's no additional encryption beyond HTTPS.
The exact format depends on the Credential transfer mode configured in the Developer Portal:
Headers (default)
Canva sends the credentials as an Authorization: Basic header, with the value Base64-encoded as <client_id>:<client_secret>, per RFC 6749 §2.3.1:
http
POST /oauth/token HTTP/1.1
Host: auth.example.com
Authorization: Basic Base64(<client_id>:<client_secret>)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<authorization_code>
&redirect_uri=<redirect_uri>Body
Canva sends client_id and client_secret as body parameters:
http
POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<authorization_code>
&redirect_uri=<redirect_uri>
&client_id=<client_id>
&client_secret=<client_secret>Headers + Body
Canva sends credentials in both the Authorization: Basic header and as body parameters:
http
POST /oauth/token HTTP/1.1
Host: auth.example.com
Authorization: Basic Base64(<client_id>:<client_secret>)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<authorization_code>
&redirect_uri=<redirect_uri>
&client_id=<client_id>
&client_secret=<client_secret>NOTE: If PKCE is enabled, Canva includes &code_verifier=<code_verifier> as an additional body parameter in all request formats above.
Recommended practices
This section describes some recommended security practices for adding OAuth support to an app.
Note: This is not a complete list of all the security hardening steps you would need to apply.
For a list of additional security responsibilities you'll need to consider for your app, see Shared responsibility model for Canva Apps.
Managing the access token lifecycle
- Canva manages the refresh and revocation of tokens on your behalf.
- If your IdP supplies a refresh token and an expiry then Canva will preemptively refresh access tokens before they become invalid. If your IdP supplies a refresh token but no expiry, Canva will assume that the token expires after a default period of time (90 days).
- To ensure that you always have an up-to-date token, always call
getAccessToken. There's no need to store or cache the token yourself or attempt to refresh the token. - If your IdP provides a token revocation endpoint, you should configure it in the developer portal to ensure that Canva can automatically revoke access to the IdP if the user's Canva account is deleted, the app is uninstalled, or when the user otherwise wants to log out of the app.
- If your IdP doesn't support refresh tokens, your user will be logged out when the token expires (provided Canva has the expiry, or the expiry is longer than 90 days) or the app will receive "unauthorized" errors from the resource server (if the expiry is shorter than 90 days).
- If you want to preemptively refresh an access token, or your IdP does not provide an expiry, you can forcefully refresh a token by calling
forceRefreshon thegetAccessTokenAPI.
Storing tokens
There's no reason to store the token, because Canva handles the caching for you. To ensure you always have an up-to-date token in a secure way, always call
getAccessToken. This method will not trigger a network request or a delay for your app, because Canva updates this value automatically.Web storage layers (such as
localstorage,cookieJar, andsession) are vulnerable to many forms of attacks that will compromise your users. For example, the risk to users of having their Google access token leaked can be devastating.There's no secure way to store a token in web storage.
Proof Key for Code Exchange (PKCE)
PKCE strengthens the OAuth flow by adding an extra layer of security, which helps ensure that access tokens are not intercepted by a malicious third-party. Not all IdPs support PKCE, but for those that do, Canva recommends keeping this feature enabled because the authentication capability takes the complexity out of using it by generating the code verifier for you.
- At the start of the OAuth flow, Canva generates a high-entropy cryptographic key called the
code_verifier. - Canva generates a
code_challenge— a SHA-256 hashed and Base64 URL-encoded version of thecode_verifier— and sends it to the authorization server in the initial authorization request. - After the user authenticates, Canva requests the access token by presenting the original
code_verifier. Before issuing an access token, the IdP compares the presented verifier with the previously supplied challenge, to ensure that they match.
Cross-Origin Opener Policy (COOP)
COOP affects Canva's OAuth flow when an application enforces same-origin or same-origin-allow-popups. These values isolate the browsing context from other origins. A same-origin or same-origin-allow-popups policy blocks Canva from accessing any authorization window properties using window.opener, preventing the OAuth flow from completing because the two windows can't communicate. Canva recommends reviewing your identity platform's COOP settings.
- If you own the identity platform where the user opens the authorization prompt, and you're enforcing COOP of
same-originorsame-origin-allow-popups, ask your security team about changing the COOP policy tounsafe-none. This policy allows the popup window to interact with the opener in a cross-origin setting. - If your app integration uses a third-party identity platform to enforce COOP of
same-originorsame-origin-allow-popups, contact your platform's support team to request changing the COOP settings.
Traffic between app and resource server
Note that https or wss requests are mandatory for all apps that communicate with external backends. This is enforced by each app's Content Security Policy (CSP). The https requirement helps ensure that sensitive data (such as tokens or user personally identifiable information (PII)) cannot be intercepted by third-parties.
However, URL parameters containing sensitive fields might be mistakenly logged. When possible, always include sensitive information in the headers or body of requests.
Separate IdPs
When using an Identity Provider (IdP), you must clearly distinguish between your production and development environments.
- Separate environments: Your production and development IdP environments must use separate environment settings, with different client IDs, secrets, and endpoints.
- User data: Don't use real user data in a development IdP.
- Logging: Your log files must not record sensitive information.
Revoking access
Tip: Review Canva's design guidelines for handling app disconnections and reconnections.
Canva can automatically request token revocation from the IdP, but only if you've configured the revocation exchange URL in the Developer Portal. For more information about the revocation setting, see Prerequisite: Configure Developer Portal.
If the revocation exchange URL is configured, Canva automatically requests token revocation in the following cases:
- The user's Canva account is deleted.
- When the users clicks on Remove from your apps:
For information on manually requesting revocation of the refresh and access tokens, see the deauthorize function.
IdP configurations
Some IdPs (such as Google) will not automatically issue refresh tokens, and require additional configuration steps. To ensure a fluid experience for users, we highly recommend setting up IdPs to issue refresh tokens.
Reusing client configuration
Some IdPs (such as Google) organize authentication and access management on a project basis, instead of by individual clients or applications. This structure can make it tempting to use a single configured client across multiple apps for simplicity. When applied poorly, this approach can be an anti-pattern for the user experience. This is because reusing a client configuration across different apps can lead to confusion during the consent phase, where users see permissions requested under a single project name, instead of permissions being specific to the app they're currently using. This shared consent experience can be misleading and might not accurately represent the individual app's data access and usage, potentially eroding trust and clarity for the end user.
To ensure a transparent and app-specific consent process, developers are advised to avoid this pattern unless there's a well-justified reason that serves a clear benefit.
Dynamic IdP configurations
If your OAuth provider requires tenant-specific subdomains (for example, https://{workspace}.example.com/oauth/authorize), you can configure wildcard URL patterns to support multi-tenant setups. For more information, see Dynamic domains OAuth.
Add OAuth to your app
To help you add OAuth support to your app, the Apps SDK includes the following methods:
- auth.initOauth: Initializes the OAuth methods.
oauth.requestAuthorization: Checks whether the user has granted authorization to your app.oauth.getAccessToken: Retrieves the user's OAuth access token.
Prerequisite: Register with third party
Register your app with the third-party platform, also known as the authorization server. This procedure varies depending on the platform. For example, this process is described in the official Google documentation.
Prerequisite: Configure Developer Portal
Once you've registered your app with the authorization server, you should have enough information to configure the OAuth settings in the developer portal.
Locate your app in the developer portal.
In the left menu, click Authentication.
Complete the settings below:
- Provider: An identifier for your authorization server. If you configure multiple providers for your app, this field is used to distinguish between them.
- Client ID: A unique identifier for your app. Provided by the authorization server.
- Client secret: Allows the authorization server to verify your app's authenticity. Provided by the authorization server.
- Credential transfer mode: Determines how Canva sends your Client ID and Client secret to the authorization server when exchanging the authorization code for tokens. Choose the mode your authorization server supports:
- Headers (default): Sends credentials as an
Authorization: Basicheader. OAuth 2.0 compliant and supported by most providers. - Body: Sends
client_idandclient_secretas body parameters. Only select if your authorization server requires it. - Headers + Body: Sends credentials in both the header and body. Not recommended unless specifically required by your authorization server.
- Headers (default): Sends credentials as an
- Authorization server URL: The URL your users are directed to when logging in with your IdP. Provided by the authorization server.
- Token exchange URL: The endpoint Canva calls to retrieve access and refresh tokens after a user logs in. Provided by the authorization server.
- Redirect URL: A read-only field pre-filled by Canva. Use the Copy button to copy this value and register it with your IdP when setting up your OAuth app.
- Revocation URL (Optional): The endpoint Canva uses to revoke access and refresh tokens if a user uninstalls the app or is disabled by an admin. Provided by the authorization server.
- PKCE: Automatically enabled to improve security. Keep this on if your IdP supports it.
Step 1: Initialize OAuth
In your app, import the required libraries and initialize the OAuth method:
ts
import { prepareDesignEditor } from "@canva/intents/design";
import { auth } from "@canva/user";
prepareDesignEditor({
async render() {
const oauth = auth.initOauth();
// ... continue rendering UI
},
});Initiating OAuth inside intents
Trigger OAuth requests from within your intent code.
<DontText>Don't initiate OAuth as a side effect at module load. Start the flow inside an intent or on explicit user interaction.</DontText>
typescript
import { auth } from "@canva/user";
const oauth = auth.initOauth();
const scope = new Set(["offline_access"]);
// Avoid: runs as soon as the module loads, not in response to an intent
await oauth.requestAuthorization({ scope });<DoText>Do initiate the flow inside an intent's renderer so the authentication overlay is rendered correctly.</DoText>
tsx
import { auth } from "@canva/user";
import { prepareDesignEditor } from "@canva/intents/design";
prepareDesignEditor({
async render() {
const oauth = auth.initOauth();
const scope = new Set(["offline_access"]);
await oauth.requestAuthorization({ scope });
// ... continue rendering UI
},
});<DoText>Do start the flow on explicit user interaction in your UI.</DoText>
tsx
import { auth } from "@canva/user";
import { prepareDesignEditor } from "@canva/intents/design";
prepareDesignEditor({
async render() {
const oauth = auth.initOauth();
const scope = new Set(["offline_access"]);
const App = () => {
const login = async () => {
await oauth.requestAuthorization({ scope });
// handle response...
};
return <button onClick={login}>Login</button>;
};
// render <App /> with your framework
},
});Per-action authorization in multi-step intents
Some intents expose multiple actions (for example, Content Publisher has getPublishConfiguration, renderSettingsUi, renderPreviewUi, and publishContent). Actions can be invoked at different times and contexts, so any action that needs to call your backend or a third‑party API should:
- Check for a valid token at the start of the action.
- If no token is available, initiate
requestAuthorizationfor that action and then proceed. - Avoid caching tokens yourself; always call
getAccessTokento read the latest token.
ts
import { auth } from "@canva/user";
import { prepareContentPublisher } from "@canva/intents/content";
const scope = new Set(["offline_access"]);
prepareContentPublisher({
async getPublishConfiguration() {
// No token required here unless your configuration endpoint is protected
return { status: "completed", outputTypes: [] };
},
renderSettingsUi() {
const oauth = auth.initOauth();
(async () => {
await oauth.requestAuthorization({ scope });
// Optional: get token if needed for API calls
const res = await oauth.getAccessToken({ scope });
const token = res?.token ?? null;
// Use token to fetch user resources (e.g., contact lists)
})();
},
renderPreviewUi() {
// No token required here unless your preview depends on protected resources
},
async publishContent(request) {
const oauth = auth.initOauth();
await oauth.requestAuthorization({ scope });
const res = await oauth.getAccessToken({ scope });
const token = res?.token ?? null;
if (!token) {
// Handle user declining/closing authorization (show UI or return an error)
return { status: "app_error", message: "Authorization required" };
}
// Use token to publish to the user's account
return { status: "completed", externalId: "...", externalUrl: "..." };
},
});Step 2: Create the state variable
Create a state variable to track the token and authorization status:
ts
export const App = () => {
const [accessTokenResponse, setAccessTokenResponse] = useState<AccessTokenResponse>(null)
const isAuthorized = useMemo(() => accessTokenResponse !== null, [accessTokenResponse])
const [isLoading, setIsLoading] = useState(true);Step 3: Create the function
Create a function that fetches an access token from OAuth and then updates the state for the accessTokenResponse variable. This will check whether the user has authorized your app's access. If the user is logged in, it will return the token:
ts
const retrieveAndSetToken = async () => {
setIsLoading(true);
try {
const accessTokenResponse = await oauth.getAccessToken({ scope });
setAccessTokenResponse(accessTokenResponse);
} finally {
setIsLoading(false);
}
};Step 4: Check for existing token
Use the useEffect hook to trigger the retrieveAndSetToken function when the component loads; this will also attempt to fetch an access token:
ts
useEffect(() => {
retrieveAndSetToken();
}, []);Step 5: Start the workflow
If no token is found, start the OAuth flow by triggering oauth.requestAuthorization(). You can then retrieve and hold the user's access token in state using the retrieveAndSetToken function created above.
ts
async function login() {
const authorizeResponse = await oauth.requestAuthorization({ scope })
if (authorizeResponse.status === "completed") {
retrieveAndSetToken()
}
}
}Step 6: Show a confirmation message
If the user has granted authorization, show a confirmation message. Otherwise, show a login prompt.
tsx
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
{isAuthorized ? (
<Text>You are logged in</Text>
) : isLoading ? (
<Text>Loading...</Text>
) : (
<Button variant="primary" onClick={login}>
Login
</Button>
)}
</Rows>
</div>
);Step 7: Fetch data from an API using the access token
Once the user is authorized and you have an access token, you can use it to make authenticated requests to your API or a third-party API.
ts
const fetchData = useCallback(async () => {
const accessToken = accessTokenResponse?.token;
if (!accessToken) {
return;
}
try {
const res = await fetch(BACKEND_URL, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const data = await res.json();
setResponseBody(data);
} catch (error) {
setError(error instanceof Error ? error.message : "Unknown error");
}
}, [accessTokenResponse]);(Optional) Step 8: Handle expired access tokens
If the IdP provides Canva with expiry times of access tokens, Canva will automatically handle the refreshing of those tokens.
If the IdP doesn't provide Canva with expiry times, Canva will treat the token as if it is expired after a default period of time (90 days). If the token expires before this time, your app will need to forcefully refresh expired tokens by setting forceRefresh to true in the getAccessToken method.
Enabling forceRefresh will either:
- Refresh the access token
- Force the user to re-authenticate if a refresh token isn't available
For example:
ts
const res = await fetch(BACKEND_URL, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!res.ok && res.status === 403) {
await retrieveAndSetToken({ forceRefresh: true });
setError("Access token expired, please try again");
return;
}
const data = await res.json();
setResponseBody(data);ts
const retrieveAndSetToken = async ({ forceRefresh = false } = {}) => {
try {
setIsLoading(true);
const accessTokenResponse = await oauth.getAccessToken({ scope, forceRefresh });
setAccessTokenResponse(accessTokenResponse);
} finally {
setIsLoading(false);
}
}Ensure refresh tokens are issued (offline access and consent)
Some providers (for example, Google) require explicit query parameters to return a refresh token consistently. When starting the authorization flow, pass the following parameters:
ts
const queryParams = new Map([
["access_type", "offline"],
["prompt", "select_account"],
]);
await oauth.requestAuthorization({ scope, queryParams });These ensure the refresh token is returned and persisted, and help avoid short-lived access tokens expiring too quickly. Consult your IdP's documentation for any provider-specific nuances.
Example code
This example is the complete code from the above tutorial.
tsx
import { useEffect, useState, useMemo, useCallback } from "react";
import type { AccessTokenResponse, Oauth } from "@canva/user";
import { auth } from "@canva/user";
import {
AppUiProvider,
Text,
Title,
Button,
Rows,
FormField,
MultilineInput,
} from "@canva/app-ui-kit";
import { createRoot } from "react-dom/client";
import { prepareDesignEditor } from "@canva/intents/design";
import * as styles from "styles/components.css";
const scope = new Set(["openid"]);
const BACKEND_URL = `${BACKEND_HOST}/custom-route`;
function DesignEditorApp({ oauth }: { oauth: Oauth }) {
const [accessTokenResponse, setAccessTokenResponse] =
useState<AccessTokenResponse>(null);
const [error, setError] = useState<string | null>(null);
const isAuthorized = useMemo(
() => accessTokenResponse != null,
[accessTokenResponse]
);
const [isLoading, setIsLoading] = useState(false);
const [responseBody, setResponseBody] = useState<unknown | undefined>(
undefined
);
const retrieveAndSetToken = async ({ forceRefresh = false } = {}) => {
try {
setIsLoading(true);
const accessTokenResponse = await oauth.getAccessToken({ scope, forceRefresh });
setAccessTokenResponse(accessTokenResponse);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
retrieveAndSetToken();
}, []);
const login = async () => {
setError(null);
const authorizeResponse = await oauth.requestAuthorization({ scope });
if (authorizeResponse.status === "completed") {
retrieveAndSetToken();
}
};
const fetchData = useCallback(async () => {
setError(null);
const accessToken = accessTokenResponse?.token;
if (!accessToken) {
return;
}
try {
const res = await fetch(BACKEND_URL, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!res.ok && res.status === 403) {
await retrieveAndSetToken({ forceRefresh: true });
setError("Access token expired, please try again");
return;
}
const data = await res.json();
setResponseBody(data);
} catch (error) {
setError(error instanceof Error ? error.message : "Unknown error");
}
}, [accessTokenResponse]);
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
{error && (
<Rows spacing="2u">
<Title>Authorization error</Title>
<Text>{error}</Text>
<Button variant="primary" onClick={login}>
Try again
</Button>
</Rows>
)}
{isAuthorized ? (
<>
<Text>You are logged in</Text>
<Button variant="primary" onClick={fetchData}>
Fetch data
</Button>
{responseBody ? (
<FormField
label="Response"
value={JSON.stringify(responseBody, null, 2)}
control={(props) => (
<MultilineInput {...props} maxRows={5} autoGrow readOnly />
)}
/>
) : null}
</>
) : isLoading ? (
<Text>Loading...</Text>
) : (
<Button variant="primary" onClick={login}>
Login
</Button>
)}
</Rows>
</div>
);
}
prepareDesignEditor({
render: async () => {
const rootElement = document.getElementById("root");
if (!(rootElement instanceof Element)) {
throw new Error("Unable to find element with id of 'root'");
}
const root = createRoot(rootElement);
const oauth = auth.initOauth();
root.render(
<AppUiProvider>
<DesignEditorApp oauth={oauth}/>
</AppUiProvider>
);
},
});API reference
For more information about the API, see auth.initOauth.
Key terms
| Term | Definition |
|---|---|
| Authorization Server | Authenticates the resource owner (typically the user), and grants or denies requests from client applications to access the user's resources on the resource server. |
| Client Identifier (or Client ID) | A unique string assigned to a client application by the authorization server. This is used to identify the client during the authorization process. Defined in RFC6749 - section 2.2 |
| Client Secret | A secret known only to the client and the Identity Provider (IdP). When combined with the client ID, it effectively creates a username and password for the client. Lets the authorization server identify the authenticity of the client (not the individual user). Defined in RFC6749 - Section 2.3.1 |
| Authorization Code | The client receives the authorization code from the authorization server. To obtain this, the client sends the resource owner to the authorization server, through their web browser. The authorization server then redirects the resource owner back to the client, along with the authorization code. Defined in RFC6749 - 1.3.1 |
| Identity Provider (IdP) | A service that authenticates users and issues tokens. In the context of Canva's OAuth integration, your IdP is the service that your users log into (for example, Google, Okta, or your own authentication server). Canva communicates directly with your IdP to complete the OAuth flow. |
| PKCE (Proof Key Code Exchange) | Improves security for clients by mitigating authorization code interception attacks. Defined in RFC7636. |
| Resource Server | The server that hosts the protected resources your app wants to access (for example, a Google Drive API or your own API). Once your app has an access token, it uses it to make requests to the resource server. |
OAuth Dynamic Domains
Configure OAuth providers with wildcard subdomains
WARNING: Dynamic domains support for OAuth is provided as a preview. Preview APIs are unstable and may change without warning. You can't release public apps using a preview API until it's stable.
Some OAuth providers require tenant-specific subdomains in their authorization URLs. For example, a provider might use https://{workspace}.example.com/oauth/authorize, where each workspace has its own subdomain.
Previously, apps had to configure a fixed URL for each tenant, making it impractical for multi-tenant scenarios. Dynamic domains support enables apps to configure a single wildcard URL pattern (for example, https://*.example.com/oauth) and dynamically replace the subdomain during the OAuth flow.
Dynamic domains support lets you:
- Configure OAuth providers with wildcard subdomains (for example,
https://*.example.com/oauth) - Use the same OAuth configuration for multiple tenants
- Collect the subdomain from users during authorization
- Automatically use the subdomain for all subsequent OAuth operations (token refresh, revocation, user info)
The SDK handles subdomain collection through a connection screen that prompts users to enter their subdomain when required. The subdomain is stored securely and used consistently throughout the OAuth lifecycle.
When to use dynamic domains
Use dynamic domains support when:
- Your OAuth provider requires tenant-specific subdomains (for example, Slack workspaces or other multi-tenant providers)
- Each user needs to specify their organization's subdomain during authentication
- Your provider uses subdomain-based routing for different customer instances
If your OAuth provider uses static URLs without subdomain requirements, use the standard OAuth integration instead.
How it works
When you configure an OAuth provider with a wildcard subdomain pattern, the following flow occurs:
Configuration: You configure the OAuth provider in the Developer Portal with a wildcard URL pattern (for example,
https://*.example.com/oauth/authorize).Authorization request: When your app calls
oauth.requestAuthorization(), Canva detects the wildcard pattern.Subdomain collection: If no subdomain is provided, Canva displays a connection screen prompting the user to enter their subdomain. The screen shows:
- App name and icon
- Domain suffix (for example, ".example.com")
- Input field for subdomain entry
- Validation feedback for invalid subdomain formats
URL replacement: Canva automatically replaces the wildcard (
*) with the user's subdomain for:- Authorization URL
- Token exchange URL
- Token refresh URL
- Token revocation URL
- User info URL (when multi-account mode is enabled)
Configuration
URL validation rules
When configuring OAuth providers with wildcard subdomains, follow these validation rules:
- Wildcard position: The wildcard (
*) must be in the subdomain position only - Single wildcard: Only one wildcard is allowed per URL
- Domain requirement: The URL must include a valid domain after the wildcard
Valid examples:
https://*.example.com/oauth/authorizehttps://*.example.com/oauth/tokenhttps://*.slack.com/api/oauth.authorize
Invalid examples:
https://example.*.com/oauth(wildcard not in subdomain position)https://*.example.*.com/oauth(multiple wildcards)https://*.com/oauth(no domain after wildcard)
Developer Portal configuration
Configure dynamic domains support in the Developer Portal:
- Navigate to your app's Authentication page.
- Select This app requires authentication.
- Select the Open Authorization protocol.
- In the Provider field, enter a name (for example,
myprovider). - In the Credentials section, configure the following URLs with wildcard patterns:
- Authorization server URL:
https://*.example.com/oauth/authorize - Token exchange URL:
https://*.example.com/oauth/token - Revocation exchange URL (optional):
https://*.example.com/oauth/revoke - User info URL (optional, for multi-account mode):
https://*.example.com/oauth/userinfo
- Authorization server URL:
<Note> You can configure combinations where the authorization URL uses a wildcard subdomain, but other fields use static URLs. However, if the authorization URL is static, other fields can't contain wildcards in the subdomain position. </Note>
For more information about configuring OAuth providers, see Prerequisite: Configure Developer Portal.
<Note> When using subdomains for OAuth providers, it's important to maintain ownership and proper lifecycle management of these subdomains to prevent security risks. For more information, see Security guidelines. </Note>
API usage
The SDK exposes the subdomain field in AccessTokenResponse and OauthAccount, allowing you to construct full URLs for API calls or differentiate between different accounts in multi-account mode.
Single-account usage
When using single-account OAuth mode, retrieve the subdomain from the AccessTokenResponse:
typescript
import { auth } from '@canva/user';
const oauth = auth.initOauth({
type: 'single_account',
provider: 'myprovider',
scope: new Set(['openid']),
});
const getBaseUrl = (subdomain: string) =>
`https://${subdomain}.example.com/api`;
async function requestData() {
let response = await oauth.getAccessToken();
if (!response) {
await oauth.requestAuthorization();
response = await oauth.getAccessToken();
}
if (!response || !response.subdomain) {
throw new Error('No access token or subdomain available');
}
return fetch(getBaseUrl(response.subdomain), {
headers: {
Authorization: `Bearer ${response.token}`,
},
});
}For more information about single-account OAuth, see OAuth integration.
Multi-account usage
When using multi-account OAuth mode, retrieve the subdomain from the OauthAccount:
typescript
import { auth, type OauthAccount } from '@canva/user';
const oauth = auth.initOauth({
type: 'multi_account',
provider: 'myprovider',
scope: new Set(['openid']),
});
const getBaseUrl = (subdomain: string) =>
`https://${subdomain}.example.com/api`;
async function requestData(account: OauthAccount) {
let response = await account.getAccessToken();
if (!response) {
await oauth.requestAuthorization();
return;
}
if (!response.subdomain) {
throw new Error('No subdomain available');
}
return fetch(getBaseUrl(response.subdomain), {
headers: {
Authorization: `Bearer ${response.token}`,
},
});
}For more information about multi-account OAuth, see Multi-account OAuth.
Backward compatibility
Dynamic domains support is fully backward compatible:
- Existing configurations: OAuth configurations without wildcards continue to work unchanged
- No breaking changes: Apps using static OAuth URLs are unaffected
- Gradual adoption: You can migrate to wildcard URLs when needed without affecting existing users
Limitations
The following limitations apply to dynamic domains support:
- Subdomain alias: Developer-configurable subdomain aliases aren't currently supported. Users must enter the exact subdomain required by their OAuth provider.
- URL combinations: While you can mix wildcard authorization URLs with static token exchange URLs, you can't use wildcards in token exchange, revocation, or user info URLs if the authorization URL is static.
- Subdomain validation: Subdomains must contain only alphanumeric characters and hyphens, and can't start or end with a hyphen.
Error handling
When using dynamic domains, be aware of these error scenarios:
- Missing subdomain: If a wildcard URL is configured but no subdomain is stored, token refresh and revocation operations will fail. In this case, the user must re-authenticate.
- Invalid subdomain format: The connection screen validates subdomain format and provides feedback to users.
- Configuration mismatch: If the OAuth configuration changes to include wildcards after accounts are created, existing accounts may need to be re-authenticated.
For more information about OAuth error handling, see OAuth troubleshooting.
Related documentation
- OAuth integration - General OAuth concepts and setup
- Multi-account OAuth - Managing multiple accounts from the same provider
- Multi-provider OAuth - Integrating with multiple OAuth providers
- OAuth troubleshooting - Error codes and troubleshooting
- Security guidelines - Security best practices for subdomain management
API reference
For more information about the OAuth API, see:
- auth.initOauth - OAuth initialization and methods
OAuth Multi-Account
Allow users to connect and manage multiple OAuth accounts from the same provider.
Multi-account OAuth enables users to connect multiple accounts from the same OAuth provider within your app. This is particularly useful for cases when users need to switch between different profiles on the same platform.
WARNING: The multi-account OAuth API is provided as a preview. Preview APIs are unstable and may change without warning. You can't release public apps using a preview API until it's stable.
Key concepts
Single-account vs multi-account mode
OAuth can be initialized in two modes:
- Single-account mode: The traditional OAuth flow where only one account per provider can be connected at a time. When a user authorizes a new account, it replaces any previously connected account.
- Multi-account mode: Allows users to connect multiple accounts from the same provider. Each account is stored separately and can be accessed, refreshed, or disconnected individually.
Provider configuration
The provider parameter identifies which OAuth configuration to use from the Developer Portal. This allows your app to configure provider-specific settings like scopes and endpoints.
Account management
In multi-account mode, each connected account includes:
- ID: A unique identifier for the account in the external provider's system
- Principal: The user's unique identifier from the OAuth provider (for example, email address)
- Display name: The user's name as provided by the OAuth provider
- Avatar URL: The user's profile picture URL
- Expiry status: Whether the account's access token has expired
When to use multi-account OAuth
Consider using multi-account OAuth when:
- Users need to manage multiple accounts from the same platform (for example, multiple social media accounts)
- Your app publishes content to external platforms and users want to choose the destination account
- Users switch between personal and business accounts on the same platform
- Your app integrates with team or organization accounts where users manage multiple profiles
If your app only needs access to a single account per provider, use the traditional OAuth flow instead.
If your app needs to integrate with multiple OAuth providers (for example, both Google and Meta), see Multi-provider OAuth.
Add multi-account OAuth to your app
Prerequisites
Before implementing multi-account OAuth:
- Configure your OAuth provider in the Developer Portal following the steps in the OAuth integration guide.
- Configure the User profile endpoint and field mappings in the Developer Portal. This endpoint is used to fetch user profile information (display name, avatar) after authentication.
Profile field mapping
When configuring your OAuth provider in the Developer Portal, you must map fields from your provider's user profile response to Canva's expected fields. This mapping tells Canva how to extract user information from your OAuth provider's profile endpoint.
The required field mappings are:
externalId: Maps to the unique user identifier from your OAuth provider (for example,subfor OpenID Connect providers,idfor many social platforms).displayName: Maps to the user's display name (for example,name,display_name, orfull_name).principal: Maps to the user's primary identifier, typically their email address (for example,email).avatarUrl(optional): Maps to the user's profile picture URL (for example,picture,avatar_url, orprofile_image_url).
For example, if your OAuth provider returns a profile response like:
json
{
"sub": "1234567890",
"name": "Jane Doe",
"email": "jane.doe@example.com",
"picture": "https://example.com/avatar.jpg"
}Your field mappings in the Developer Portal would be:
externalId:subdisplayName:nameprincipal:emailavatarUrl:picture
NOTE: The field mapping configuration is critical for multi-account OAuth to work correctly. Incorrect mappings prevents Canva from properly identifying and displaying user accounts.
Important provider configuration notes
WARNING: When you create an OAuth provider in the Developer Portal, you can't change the multi-account setting. The multi-account flag is immutable after creating an OAuth provider.
If you need to switch between single-account and multi-account modes:
- Delete the existing provider from the Developer Portal.
- Create a new provider with the desired multi-account setting.
- Reconfigure all settings including endpoints, scopes, and field mappings.
Impact of provider deletion:
- All existing user authentications will be revoked. Users who previously connected their accounts will need to re-authenticate.
- All stored access and refresh tokens will be invalidated. Your app will lose access to external APIs for all users.
- User data associated with the provider may be lost. Depending on how your app stores user information.
Consider the multi-account requirement carefully during initial setup to avoid disrupting existing users.
Step 1: Initialize OAuth in multi-account mode
Import the required libraries and initialize OAuth with multi-account options:
ts
import { auth } from "@canva/user";
const oauth = auth.initOauth({
type: "multi_account",
provider: "meta",
});Step 2: Create state variables
Create state variables to track connected accounts and the selected account:
tsx
import { useState, useEffect } from "react";
import type { OauthAccount } from "@canva/user";
const scope = ["profile", "email"];
export const App = () => {
const [accounts, setAccounts] = useState<OauthAccount[]>([]);
const [selectedAccount, setSelectedAccount] = useState<OauthAccount | null>(null);
const [isLoading, setIsLoading] = useState(true);Step 3: Fetch connected accounts
Create a function to fetch and update the list of connected accounts:
ts
const refetchAccounts = async () => {
setIsLoading(true);
try {
const response = await oauth.getAccounts();
setAccounts(response.accounts);
// Set first account as selected if none is currently selected
if (!selectedAccount && response.accounts.length > 0) {
setSelectedAccount(response.accounts[0]);
}
} catch (error) {
console.error("Failed to fetch accounts:", error);
} finally {
setIsLoading(false);
}
};Step 4: Load accounts on mount
Use the useEffect hook to load accounts when the component mounts:
ts
useEffect(() => {
refetchAccounts();
}, []);Step 5: Implement authorization flow
Create a function to authorize new accounts:
ts
const authorize = async () => {
try {
// Request authorization from the OAuth provider
// This opens a popup window for the user to log in
await oauth.requestAuthorization({ scope });
// Refresh the accounts list after successful authorization
await refetchAccounts();
} catch (error) {
console.error("Authorization failed:", error);
}
};Step 6: Implement account disconnection
Create a function to disconnect individual accounts:
ts
const disconnectAccount = async (account: OauthAccount) => {
try {
// Revoke access for the specific account
await account.deauthorize();
// Clear selected account if it was the one being disconnected
if (selectedAccount?.id === account.id) {
setSelectedAccount(null);
}
// Refresh the accounts list
await refetchAccounts();
} catch (error) {
console.error("Failed to disconnect account:", error);
}
};Step 7: Display account list and controls
Render the list of connected accounts with controls to select or disconnect them:
tsx
import { Button, Text, Title, Rows } from "@canva/app-ui-kit";
return (
<Rows spacing="2u">
{isLoading ? (
<Text>Loading accounts...</Text>
) : accounts.length === 0 ? (
<>
<Text>No accounts connected</Text>
<Button variant="primary" onClick={authorize}>
Connect account
</Button>
</>
) : (
<>
{accounts.map((account) => (
<Rows key={account.id} spacing="1u">
<Title>{account.displayName}</Title>
<Text size="small">{account.principal}</Text>
{account.expired && (
<Text size="small" tone="critical">
Access expired - reconnect required
</Text>
)}
{selectedAccount?.id === account.id ? (
<Text tone="info">Currently selected</Text>
) : (
<Button
variant="secondary"
onClick={() => setSelectedAccount(account)}
>
Select
</Button>
)}
<Button
variant="secondary"
onClick={() => disconnectAccount(account)}
>
Disconnect
</Button>
</Rows>
))}
<Button variant="primary" onClick={authorize}>
Connect another account
</Button>
</>
)}
</Rows>
);Step 8: Use access tokens for API requests
When making API requests, use the access token from the selected account:
ts
const fetchData = async () => {
if (!selectedAccount) {
return;
}
try {
// Get the access token for the selected account
const tokenResponse = await selectedAccount.getAccessToken();
if (!tokenResponse) {
// Access token not available, user needs to re-authorize
console.error("No access token available");
return;
}
// Make an authenticated API request
const response = await fetch("https://api.example.com/data", {
headers: {
Authorization: `Bearer ${tokenResponse.token}`,
},
});
const data = await response.json();
console.log("API response:", data);
} catch (error) {
console.error("Failed to fetch data:", error);
}
};Recommended practices
Account identification
- Use the
principalfield (for example, email address) to help users identify their accounts - Display the
displayNameandavatarUrlto provide visual context - Show the
expiredstatus to alert users when re-authorization is needed
User experience
- Clearly indicate which account is currently selected
- Provide a way to switch between accounts without disconnecting
- Make it easy to add additional accounts
- Show account information (name, profile picture) in selection interfaces
Error handling
- Handle cases where
getAccessToken()returnsnull(token unavailable or expired) - Provide clear error messages when authorization fails
- Gracefully handle network errors when fetching account lists
- Prompt users to re-authorize when tokens expire
Security
- Always call
getAccessToken()to get the latest token; don't cache tokens yourself - Use the
deauthorize()method to properly revoke access when disconnecting accounts - Validate that the selected account exists before making API requests
- Handle token refresh automatically by relying on Canva's token management
API reference
For more information about the multi-account OAuth API, see:
- auth.initOauth
OAuth Multi-Provider
Allow users to connect to multiple OAuth providers simultaneously.
Multi-provider OAuth enables your app to integrate with multiple OAuth providers (for example, Meta and Google) simultaneously, allowing users to connect to different platforms independently. Each provider maintains its own authentication state and tokens.
Key concepts
Multi-provider vs multi-account
It's important to understand the difference between these two concepts:
- Multi-provider OAuth: Your app connects to multiple different OAuth providers (for example, Meta and Google). Each provider is configured separately and maintains independent authentication state.
- Multi-account OAuth: Your app connects to multiple accounts from the same OAuth provider (for example, multiple Meta accounts). See Multi-account OAuth for more information.
You can combine both approaches: use multi-provider OAuth to support multiple providers, and use multi-account OAuth for each provider to allow multiple accounts per provider.
Provider configuration
Each OAuth provider must be configured separately in the Developer Portal. The provider parameter identifies which OAuth configuration to use:
- Each provider has its own authorization endpoint, token endpoint, client ID, and client secret
- Providers can have different scopes and settings
- Each provider maintains independent authentication state
Independent state management
When using multiple providers, each provider's authentication state is managed independently:
- Tokens are stored separately for each provider
- Authorization flows are independent
- Users can connect or disconnect providers independently
- API requests use provider-specific tokens
When to use multi-provider OAuth
Consider using multi-provider OAuth when:
- Your app needs to integrate with multiple external platforms (for example, social media platforms, cloud storage services)
- Users need to connect to different services simultaneously
- Your app publishes content to multiple platforms and users need to choose destinations
- You want to provide users with flexibility to connect only the services they use
If your app only needs to integrate with a single OAuth provider, use the traditional OAuth flow instead.
Add multi-provider OAuth to your app
Prerequisites
Before implementing multi-provider OAuth:
- Configure each OAuth provider in the Developer Portal following the steps in the OAuth integration guide.
- Each provider needs to be configured separately with its own:
- Authorization endpoint
- Token endpoint
- Client ID and Client Secret
- Provider name (used to identify the provider in your code)
For example, if you're integrating with Meta and Google, you would configure:
- A Meta provider with provider name
meta - A Google provider with provider name
google
Step 1: Initialize OAuth clients for each provider
Import the required libraries and initialize separate OAuth clients for each provider:
ts
import { auth } from "@canva/user";
// Provider names as defined in the Developer Portal
const META_PROVIDER = "meta" as const;
const GOOGLE_PROVIDER = "google" as const;
// Initialize separate OAuth clients for each provider
const metaOauth = auth.initOauth({
type: "single_account",
provider: META_PROVIDER,
});
const googleOauth = auth.initOauth({
type: "single_account",
provider: GOOGLE_PROVIDER,
});NOTE: You can use multi_account type instead of single_account if you want to support multiple accounts per provider. See Multi-account OAuth for more information.
Step 2: Create state variables for each provider
Create separate state variables to track the authentication status for each provider:
tsx
import { useState, useEffect, useMemo, useCallback } from "react";
import type { AccessTokenResponse } from "@canva/user";
type ProviderStatus = {
accessTokenResponse: AccessTokenResponse | null | undefined;
error: string | null;
loading: boolean;
responseBody: unknown | undefined;
};
export const App = () => {
// Track state for each provider independently
const [metaStatus, setMetaStatus] = useState<ProviderStatus>({
accessTokenResponse: undefined,
error: null,
loading: false,
responseBody: undefined,
});
const [googleStatus, setGoogleStatus] = useState<ProviderStatus>({
accessTokenResponse: undefined,
error: null,
loading: false,
responseBody: undefined,
});Step 3: Create helper functions
Create helper functions to manage provider-specific operations:
tsx
// Define provider-specific scopes
const metaScope = new Set(["openid"]);
const googleScope = new Set(["openid", "profile", "email"]);
// Helper function to get the OAuth client for a provider
const getOauthClient = (provider: typeof META_PROVIDER | typeof GOOGLE_PROVIDER) => {
return provider === META_PROVIDER ? metaOauth : googleOauth;
};
// Helper function to get scopes for a provider
const getScope = (provider: typeof META_PROVIDER | typeof GOOGLE_PROVIDER) => {
return provider === META_PROVIDER ? metaScope : googleScope;
};
// Helper function to get status for a provider
const getStatus = (provider: typeof META_PROVIDER | typeof GOOGLE_PROVIDER) => {
return provider === META_PROVIDER ? metaStatus : googleStatus;
};
// Helper function to set status for a provider
const setStatus = (
provider: typeof META_PROVIDER | typeof GOOGLE_PROVIDER,
updates: Partial<ProviderStatus>,
) => {
if (provider === META_PROVIDER) {
setMetaStatus((prev) => ({ ...prev, ...updates }));
} else {
setGoogleStatus((prev) => ({ ...prev, ...updates }));
}
};Step 4: Check existing authentication on mount
Check if users are already authenticated when the component mounts:
tsx
useEffect(() => {
// Check if users are already authenticated when the component mounts
retrieveAndSetToken(META_PROVIDER);
retrieveAndSetToken(GOOGLE_PROVIDER);
}, [metaOauth, googleOauth]);Step 5: Implement authorization flow
Create a function to authorize users for a specific provider:
tsx
const authorize = useCallback(
async (provider: typeof META_PROVIDER | typeof GOOGLE_PROVIDER) => {
const oauth = getOauthClient(provider);
const scope = getScope(provider);
setStatus(provider, {
accessTokenResponse: undefined,
error: null,
loading: true,
});
try {
// Trigger the OAuth authorization flow for the specific provider
await oauth.requestAuthorization({ scope });
await retrieveAndSetToken(provider);
} catch (error) {
setStatus(provider, {
error: error instanceof Error ? error.message : "Unknown error",
loading: false,
});
}
},
[metaOauth, googleOauth],
);Step 6: Retrieve and manage access tokens
Create a function to retrieve access tokens for a provider:
tsx
// IMPORTANT: Always call getAccessToken when you need a token - tokens can expire.
// Canva automatically handles caching and refreshing tokens for you.
const retrieveAndSetToken = useCallback(
async (
provider: typeof META_PROVIDER | typeof GOOGLE_PROVIDER,
forceRefresh = false,
) => {
const oauth = getOauthClient(provider);
const scope = getScope(provider);
setStatus(provider, { loading: true });
try {
const accessTokenResponse = await oauth.getAccessToken({
forceRefresh,
scope,
});
setStatus(provider, {
accessTokenResponse,
loading: false,
});
} catch (error) {
setStatus(provider, {
error: error instanceof Error ? error.message : "Unknown error",
loading: false,
});
}
},
[metaOauth, googleOauth],
);Step 7: Implement logout
Create a function to disconnect a provider:
tsx
const logout = useCallback(
async (provider: typeof META_PROVIDER | typeof GOOGLE_PROVIDER) => {
const oauth = getOauthClient(provider);
setStatus(provider, {
accessTokenResponse: undefined,
loading: true,
});
// Revoke the user's authorization and clear stored tokens for the specific provider
await oauth.deauthorize();
setStatus(provider, {
accessTokenResponse: null,
loading: false,
responseBody: undefined,
});
},
[metaOauth, googleOauth],
);Step 8: Make authenticated API requests
When making API requests, use the access token from the appropriate provider:
tsx
const fetchData = useCallback(
async (provider: typeof META_PROVIDER | typeof GOOGLE_PROVIDER) => {
const status = getStatus(provider);
const accessToken = status.accessTokenResponse?.token;
if (!accessToken) {
return;
}
// Use provider-specific backend URL
const backendUrl =
provider === META_PROVIDER ? META_BACKEND_URL : GOOGLE_BACKEND_URL;
setStatus(provider, { loading: true });
try {
// Example of using the access token to make authenticated API requests
const res = await fetch(backendUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const data = await res.json();
setStatus(provider, {
responseBody: data,
loading: false,
});
} catch (error) {
setStatus(provider, {
error: error instanceof Error ? error.message : "Unknown error",
loading: false,
});
}
},
[metaStatus.accessTokenResponse, googleStatus.accessTokenResponse],
);Step 9: Display provider sections in UI
Create a reusable component to render the UI for each provider:
tsx
import { Button, Text, Title, Rows, Box, LoadingIndicator } from "@canva/app-ui-kit";
const renderProviderSection = (
provider: typeof META_PROVIDER | typeof GOOGLE_PROVIDER,
providerDisplayName: string,
) => {
const status = getStatus(provider);
const loading = status.loading && status.accessTokenResponse === undefined;
const isConnected = status.accessTokenResponse != null;
return (
<Box
padding="3u"
border="standard"
borderRadius="standard"
background="neutral"
>
<Rows spacing="2u">
<Title size="small">{providerDisplayName} Provider</Title>
{status.error ? (
<Rows spacing="1u">
<Text tone="critical">Error: {status.error}</Text>
<Button
variant="primary"
onClick={() => authorize(provider)}
disabled={loading}
>
Try again
</Button>
</Rows>
) : loading ? (
<LoadingIndicator />
) : !isConnected ? (
<Rows spacing="2u">
<Text>
Connect your {providerDisplayName} account to use this provider.
</Text>
<Button
variant="primary"
onClick={() => authorize(provider)}
disabled={loading}
>
{`Sign in to ${providerDisplayName}`}
</Button>
</Rows>
) : (
<Rows spacing="2u">
<Text>Connected to {providerDisplayName}!</Text>
<Button
variant="secondary"
onClick={() => logout(provider)}
disabled={loading}
>
Log out
</Button>
<Button
variant="primary"
onClick={() => fetchData(provider)}
disabled={loading}
>
Fetch data
</Button>
</Rows>
)}
</Rows>
</Box>
);
};
// Use in your component
return (
<Rows spacing="3u">
<Title>Multi-provider authentication</Title>
{renderProviderSection(META_PROVIDER, "Meta")}
{renderProviderSection(GOOGLE_PROVIDER, "Google")}
</Rows>
);Complete example
For a complete working example, see the multi-provider authentication example in the starter kit.
Recommended practices
Provider management
- Initialize OAuth clients using
useMemoto avoid recreating them on every render - Use consistent provider names that match your Developer Portal configuration
- Create helper functions to manage provider-specific operations
State management
- Track state independently for each provider
- Use separate state variables or a state object keyed by provider
- Handle loading and error states per provider
User experience
- Clearly indicate which provider each section represents
- Show connection status for each provider independently
- Allow users to connect or disconnect providers independently
- Provide clear error messages specific to each provider
Token management
- Always call
getAccessToken()when you need a token; don't cache tokens yourself - Canva automatically handles token caching and refreshing
- Use provider-specific tokens for API requests
- Handle token expiration gracefully per provider
Error handling
- Handle errors independently for each provider
- Provide clear error messages when authorization fails
- Allow users to retry authorization for specific providers
- Gracefully handle network errors when making API requests
Security
- Use the
deauthorize()method to properly revoke access when disconnecting providers - Validate that tokens exist before making API requests
- Handle token refresh automatically by relying on Canva's token management
- Never expose tokens in client-side code or logs
Related documentation
- OAuth integration: Single-provider OAuth authentication
- Multi-account OAuth: Multiple accounts from the same provider
- Multi-provider authentication example: Complete working example
API reference
For more information about the OAuth API, see:
- auth.initOauth
OAuth Troubleshooting
Reference for OAuth error codes.
This article lists OAuth error codes, their causes, and actions you can take to resolve them. Your app should catch all these errors and handle them appropriately.
Canva handles the OAuth flow and token retrieval, so you might need to contact Canva support for certain errors.
Error types
The requestAuthorization and getAccessToken methods can throw two types of errors:
CanvaErrorOauthError. A subclass ofCanvaError, this contains anoauthCodeproperty and acodeproperty. The possible values are listed in the following tables.
To handle errors, we recommend displaying a message to let the user know that something has gone wrong, and offer them an opportunity to retry. You can then extend this approach, for example, if the user denies the request, then offer reassurance that the app requires them to login and grant permissions. Your app can also retry transient errors, such as server errors.
Error codes
oauthCode | code | Details |
|---|---|---|
invalid_request | bad_request | <ul>- Cause: The request is missing required parameters, has invalid values, or is malformed (for example, duplicated parameters).- Manifests by: Identity provider-specific requirements that are separate from the OAuth protocol.- Fix: Check that all required parameters are included, make sure values are valid, and avoid duplicates. This might be fixed with the query_params argument, but might also be a Canva issue.</ul> |
access_denied | permission_denied | <ul>- Cause: The user or authorization server denied the request.- Manifests by: <ul>- User denies the app permissions for Canva to access the resources.</ul><ul>- App server denies the access because the resource owner or authorization server denying the request.</ul>- Fix: Notify the user and suggest retrying the authorization, making sure they approve the request.</ul> |
unauthorized_client | not_allowed | <ul>- Cause: The client isn't authorized to request authorization using this method.- Manifests by: Identity provider might restrict certain clients (like mobile), or the client might be disabled or blocked.- Fix: Verify that the client has the correct permissions and is using a valid grant type.</ul> |
invalid_client | bad_request | <ul>- Cause: Client authentication failed (for example, an unknown client or unsupported authentication method).- Manifests by: Client authentication failed, possibly because of an incorrect client URL, secret, or ID.- Fix: Make sure that the client ID and secret are correct, and that a supported authentication method is used.</ul> |
invalid_scope | bad_request | <ul>- Cause: The requested scope is invalid, unknown, or malformed.- Manifests by: Introduced in the app code. Also occurs when requesting a scope the user doesn’t have access to, or the user's scope has been revoked.- Fix: Confirm that the requested scope is valid and supported by the authorization server.</ul> |
server_error | bad_external_service_response | <ul>- Cause: An unexpected internal server issue occurred.- Fix: Retry the request after a short delay, or contact support if the issue persists.</ul> |
temporarily_unavailable | bad_external_service_response | <ul>- Cause: The server is temporarily overloaded or undergoing maintenance.- Fix: Wait and retry the request later. Monitor the server status, if possible.</ul> |
invalid_grant | bad_request | <ul>- Cause: The provided authorization grant or refresh token is invalid, expired, revoked, or doesn't match the original request.- Manifests by: Grant token or refresh token is incorrect, invalid, expired, or revoked.- Fix: Unlikely to occur, but retry the authorization call again, and make sure that the auth flows are supported. </ul> |
Error codes for identity providers
The following errors can occur because of the way an identity provider has implemented OAuth. As a result, you might not be able to resolve them yourself, and might have to contact your identity provider's support team.
oauthCode | code | Details |
|---|---|---|
unsupported_response_type | bad_request | <ul>- Cause: The server doesn't support the requested response type.- Manifests by: For example, some identity providers expect requests with specific headers.- Fix: You might not be able to fix this yourself, because it's likely the identity provider is incompatible.</ul> |
unsupported_grant_type | bad_request | <ul>- Cause: The server doesn't support the requested grant type.- Fix: You might not be able to fix this yourself, because it's likely the identity provider is incompatible. The authorization code grant flow or refresh token flow might not be supported by the identity provider.</ul> |
unsupported_token_type | bad_request | <ul>- Cause: The requested token type isn't supported by the server.- Fix: You might not be able to fix this yourself, because it's likely the identity provider is incompatible.</ul> |
Testing OAuth
When testing your app's OAuth integration, we recommended building a test plan that includes these practices:
- Scenario exploration: Document how you think these errors might manifest themselves in your app.
- Test data: Link your test data used for these scenarios.
- Management approach: Describe how you will manage these errors and provide sample screenshots.
Test environments and users
It's good practice to test using a comprehensive list of users and roles that are reflective of your user base. This helps ensure that your users have a good authorization experience, and see the right data.
For example, these questions can help you get started:
Planning for account types and user roles:
- What account types and user roles will your end users use with this app?
- What scopes do the users have access to?
Test environment: User account data
- List links to the test environment, user account, and test data that was used during testing. This should be representative of the types of users that will use the app. For example:
- User Account:
Joe Bloggs - Environment (link):
[Link to Environment] - Notes:
[Additional Notes]
- User Account:
- List links to the test environment, user account, and test data that was used during testing. This should be representative of the types of users that will use the app. For example:
Basic test flow
To help you get started, this is an example of a basic OAuth test flow. This isn't a complete list of everything you should test.
- Users can successfully log in and log out.
- If users grant access using the consent prompt, they are then given access to the data.
- If users deny access to the consent prompt, they aren't logged in.
- Token refresh and expiry are working as expected.
- If the user removes the app and then uses it again, they are prompted for reauthorization.
- OAuth works on the Canva desktop application and mobile website.
Alternative Auth Experiences
Alternatives for non-standard authentication scenarios.
Managing sensitive information like API keys responsibly is an important part of building secure, user-trusted apps.
This guide outlines preferred patterns for managing user authentication and sensitive information, with solutions tailored to the capabilities and constraints of Canva's environment.
Before you begin, review the Authentication design guidelines.
When not to use alternative authentication
Alternative authentication patterns should only be used when no Canva pattern can support your use case. In almost all situations, you should avoid building custom auth flows and instead use one of the following:
- Frictionless authentication
- OAuth integration
If your app can be redesigned to avoid handling secrets in the frontend — or to rely on Canva’s built-in authentication APIs — you should always choose those options first.
Alternative authentication is a last resort, and should be used only when the recommended approaches cannot meet functional or product requirements.
For a full explanation of Canva’s authentication model, see Authenticating users.
Key principles for success
- Design for minimal exposure. Secrets should only be handled in the frontend when absolutely necessary and for the shortest possible time.
- Prioritize secure backend operations. Whenever possible, use our OAuth integration which handles the OAuth flow and the management of access and refresh tokens for you.
- Use short-lived, scoped tokens. When secrets must be used in the frontend, prefer API keys that expire and are limited in what they can access.
Recommended approaches
Frictionless authentication (preferred)
Best for: Apps that don’t require traditional login upfront, or may not need authentication at all.
You can use the auth.getCanvaUserToken() method to access a signed-in user’s data without requiring a separate login flow. This approach enables users to interact with your app immediately (no password prompts or popups), allowing them to explore functionality first and authenticate only when necessary.
✅ Why choose it:
- Reduces user friction by avoiding login walls.
- Lets users experience value before being asked to authenticate.
- Simplifies the developer experience; no token handling required.
See Frictionless authentication.
OAuth integration (recommended for third-party access)
Best for: Apps that connect to external platforms like Google Drive, Dropbox, or similar.
OAuth allows users to securely authorize your app to access third-party resources without exposing their credentials. Canva simplifies this process by handling the full OAuth flow (including token exchange and refreshes) on your behalf.
✅ Why choose it:
- Canva manages both access and refresh tokens securely.
- Reduces your app’s need to store or handle secrets directly.
- Aligns with industry standards using the OAuth 2 Authorization Code flow (with or without Proof Key Code Exchange (PKCE)).
This approach not only strengthens security but also improves the user experience by streamlining how users grant permissions. Canva's authentication API takes care of all server-side interactions with your selected Identity Provider (IdP).
See OAuth integration.
JavaScript closures for temporary secret storage (use only when needed)
WARNING: Closures should only be used when other, more secure options aren't viable.
Best for: Temporarily holding secrets in memory during a session when backend-based or Canva-managed storage isn’t feasible, for example API keys.
In rare cases where secrets must be handled directly in the frontend, a JavaScript closure offers a lightweight way to store them temporarily in memory.
ts
function createSecretStore() {
let secret = null;
return {
setSecret(newSecret) {
secret = newSecret;
},
getSecret() {
return secret;
},
clearSecret() {
secret = null;
}
};
}
const secretStore = createSecretStore();Use responsibly:
- Set secrets only when needed, and clear them promptly.
- Never expose secret storage functions globally.
- Understand that this offers limited protection — secrets can still be accessed through memory inspection.
What to avoid
To maintain a secure app environment, avoid using storage APIs and browser features that persist data in an inspectable or unsafe way:
<DontText>Avoid:</DontText>
localStorage,sessionStorage, andIndexedDB— not encrypted and easily inspected.ts// ❌ Don't localStorage.setItem('apiKey', apiKey); sessionStorage.setItem('userToken', userToken);Embedding secrets in your JavaScript bundle.
ts// ❌ Don't const token = 'api-key-123'; // ...Logging secrets to the browser console or exposing them through query parameters.
ts// ❌ Don't console.log('API key:', token); fetch(`https://api.example.com/data?api_key=${token}`);
Encoding or encrypting secrets in WebStorage
It's a common misconception that encoding (for example, Base64) or even encrypting secrets in the frontend before storing them in localStorage or sessionStorage can make them safe.
While these techniques may obscure the data, they don't secure it.
Here's why:
- Base64 isn't encryption. It's merely an encoding format. Anyone can decode it with minimal effort.
- Frontend encryption still requires access to a key. If you encrypt something in the browser, the encryption key also needs to be present in the browser, which means a user (or an attacker) can extract both the encrypted data and the key. If both are accessible, the encryption provides no meaningful protection.
- WebStorage is inspectable. All values stored in
localStorageorsessionStoragecan be viewed, copied, and manipulated using browser developer tools, regardless of whether they’re encrypted. - Encryption doesn't protect against Cross-Site Scripting. If an attacker exploits a cross-site scripting (XSS) vulnerability, they can access both the stored encrypted value and any in-memory decryption logic or keys, rendering encryption ineffective.