Multi-provider Authentication

Multiple OAuth provider authentication integration for external platforms.

Running this example

To run this example locally:

  1. If you haven't already, create a new app in the Developer Portal(opens in a new tab or window). For more information, refer to our Quickstart guide.

  2. In your app's configuration on the Developer Portal(opens in a new tab or window), ensure the "Development URL" is set to http://localhost:8080.

  3. Clone the starter kit:

    git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.git
    cd canva-apps-sdk-starter-kit
    SHELL
  4. Install dependencies:

    npm install
    SHELL
  5. Run the example:

    npm run start multi_provider_authentication
    SHELL
  6. Click the Preview URL link shown in the terminal to open the example in the Canva editor.

Example app source code

// For usage information, see the README.md file.
import {
Button,
LoadingIndicator,
Rows,
Title,
Text,
Box,
MultilineInput,
FormField,
Columns,
Column,
} from "@canva/app-ui-kit";
import { useMemo, useState, useEffect, useCallback } from "react";
import type { AccessTokenResponse } from "@canva/user";
import { auth } from "@canva/user";
import * as styles from "styles/components.css";
// Provider names as defined in the Developer Portal
const META_PROVIDER = "meta" as const;
const GOOGLE_PROVIDER = "google" as const;
const metaScope = new Set(["openid"]);
const googleScope = new Set(["openid", "profile", "email"]);
const META_BACKEND_URL = `${BACKEND_HOST}/meta-route`;
const GOOGLE_BACKEND_URL = `${BACKEND_HOST}/google-route`;
type ProviderStatus = {
accessTokenResponse: AccessTokenResponse | null | undefined;
error: string | null;
loading: boolean;
responseBody: unknown | undefined;
};
export function App() {
// Initialize separate OAuth clients for each provider
const metaOauth = useMemo(
() => auth.initOauth({ type: "single_account", provider: META_PROVIDER }),
[],
);
const googleOauth = useMemo(
() =>
auth.initOauth({
type: "single_account",
provider: GOOGLE_PROVIDER,
}),
[],
);
// 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,
});
useEffect(() => {
// Check if users are already authenticated when the component mounts
retrieveAndSetToken(META_PROVIDER);
retrieveAndSetToken(GOOGLE_PROVIDER);
}, [metaOauth, googleOauth]);
const getOauthClient = (
provider: typeof META_PROVIDER | typeof GOOGLE_PROVIDER,
) => {
return provider === META_PROVIDER ? metaOauth : googleOauth;
};
const getScope = (
provider: typeof META_PROVIDER | typeof GOOGLE_PROVIDER,
) => {
return provider === META_PROVIDER ? metaScope : googleScope;
};
const getStatus = (
provider: typeof META_PROVIDER | typeof GOOGLE_PROVIDER,
) => {
return provider === META_PROVIDER ? metaStatus : googleStatus;
};
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 }));
}
};
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],
);
// 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],
);
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],
);
const fetchData = useCallback(
async (provider: typeof META_PROVIDER | typeof GOOGLE_PROVIDER) => {
const status = getStatus(provider);
const accessToken = status.accessTokenResponse?.token;
if (!accessToken) {
return;
}
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],
);
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>
<Columns spacing="1u">
<Column>
<Button
variant="secondary"
onClick={() => logout(provider)}
disabled={loading}
>
Log out
</Button>
</Column>
<Column>
<Button
variant="primary"
onClick={() => fetchData(provider)}
disabled={loading}
>
Fetch data
</Button>
</Column>
</Columns>
{status.responseBody ? (
<FormField
label="Response"
value={JSON.stringify(status.responseBody, null, 2)}
control={(props) => (
<MultilineInput {...props} maxRows={5} autoGrow readOnly />
)}
/>
) : null}
</Rows>
)}
</Rows>
</Box>
);
};
const result = (
<div className={styles.scrollContainer}>
<Box
justifyContent="center"
width="full"
alignItems="center"
display="flex"
height="full"
>
<Rows spacing="3u">
<Rows spacing="2u">
<Title>Multi-provider authentication</Title>
<Text>
This example demonstrates how apps can allow users to authorize
with multiple OAuth providers (Meta and Google) simultaneously.
</Text>
<Text>
To set up please see the README.md in the
/examples/fundamentals/multi_provider_authentication folder
</Text>
</Rows>
{renderProviderSection(META_PROVIDER, "Meta")}
{renderProviderSection(GOOGLE_PROVIDER, "Google")}
</Rows>
</Box>
</div>
);
return result;
}
TYPESCRIPT
// For usage information, see the README.md file.
import { AppUiProvider } from "@canva/app-ui-kit";
import { createRoot } from "react-dom/client";
import { App } from "./app";
import "@canva/app-ui-kit/styles.css";
import type { DesignEditorIntent } from "@canva/intents/design";
import { prepareDesignEditor } from "@canva/intents/design";
async function render() {
const root = createRoot(document.getElementById("root") as Element);
root.render(
<AppUiProvider>
<App />
</AppUiProvider>,
);
}
const designEditor: DesignEditorIntent = { render };
prepareDesignEditor(designEditor);
// Hot Module Replacement for development (automatically reloads the app when changes are made)
if (module.hot) {
module.hot.accept("./app", render);
}
TYPESCRIPT
# Multi-provider authentication
Demonstrates how to implement OAuth authentication with multiple providers (Meta and Google) in a single app, allowing users to connect to different platforms simultaneously. Shows provider-specific token management, independent authorization flows, and authenticated API requests using tokens from different providers.
For API reference docs and instructions on running this example, see: https://www.canva.dev/docs/apps/examples/multi-provider-authentication/.
Related examples: See fundamentals/authentication for single-provider OAuth authentication, fundamentals/multi_account_authentication for multiple accounts from a single provider, or design_interaction/design_token for design-specific authentication patterns.
NOTE: This example differs from what is expected for public apps to pass a Canva review:
- Token storage and security is simplified for demonstration. Production apps must implement secure token storage and follow OAuth security best practices
- Error handling for authentication failures is simplified for demonstration. Production apps must implement comprehensive error handling with clear user feedback and graceful failure modes
- Token refresh mechanisms are not implemented. Production apps should implement proper token lifecycle management
- Internationalization is not implemented. Production apps must support multiple languages using the `@canva/app-i18n-kit` package to pass Canva review requirements
- The code structure is simplified: Production apps using [intents](https://www.canva.dev/docs/apps/intents/) are recommended to call the prepareDesignEditor function from src/intents/design_editor/index.tsx
MARKDOWN
# Setup
## Getting started
Before using this example, you'll need to configure multiple OAuth providers in the [Developer Portal](https://www.canva.com/developers/apps):
1. **Configure Meta OAuth provider**: Follow the [OAuth integration guide](https://www.canva.dev/docs/apps/authenticating-users/oauth#prerequisite-configure-developer-portal) to set up a Meta provider with provider name `meta`.
2. **Configure Google OAuth provider**: Follow the same guide to set up a Google provider with provider name `google`.
Each provider needs to be configured separately in the Developer Portal with its own:
- Authorization endpoint
- Token endpoint
- Client ID and Client Secret
Once both providers are configured, simply run the example from the root of `canva-apps-sdk-starter-kit` with:
```sh
npm start multi_provider_authentication
```
MARKDOWN

API reference

Need help?