Skip to content

Instantly share code, notes, and snippets.

@wilsonowilson
Last active July 22, 2024 02:26
Show Gist options
  • Save wilsonowilson/1ddc5407d8466a150bb283fad9f741ae to your computer and use it in GitHub Desktop.
Save wilsonowilson/1ddc5407d8466a150bb283fad9f741ae to your computer and use it in GitHub Desktop.
Setting up ConvertKit Plugin Oauth with Typescript + Firebase
<script lang="ts">
import { page } from '$app/stores';
import { getInitialUser } from '$lib/Identity/api/auth';
import AuthPageLayout from '$lib/Identity/components/AuthPageLayout.svelte';
import InlineLoader from '@senja/shared/components/InlineLoader.svelte';
import TertiaryButton from '@senja/shared/components/TertiaryButton.svelte';
import { ConvertkitIcon } from '@senja/shared/components/icons/integrations';
import { optimizeImage } from '@senja/shared/utils/cdn';
// This is the first URL ConvertKit will call. It'll contain a redirect URL, a <state> parameter and a client_id.
// Once you've gained the user's consent, redirect them to Convertkit's redirect_uri.
// You can use any JS framework of your choice, but in this example I'm using Svelte.
let loading = false;
let signUpLink =
'https://app.senja.io/signup?utm_campaign=integrations&utm_medium=partner&utm_source=convertkit&ref=convertkit-integration';
async function completeOauth() {
loading = true;
const currentUser = await getInitialUser();
if (!currentUser) {
window.location.href = signUpLink;
return;
}
window.location.href =
$page.url.searchParams.get('redirect_uri') +
`?state=${$page.url.searchParams.get('state')}&code=${
currentUser.uid
}&client_id=${$page.url.searchParams.get('client_id')}`;
}
</script>
<AuthPageLayout integrationIcon={optimizeImage(ConvertkitIcon.src, { format: 'webp', width: 200 })}>
<h1 slot="header" class="text-2xl mt-4 font-medium">
Connect your Senja account to Convertkit
</h1>
<p slot="description" class="text-gray-500">
Senja helps you start collecting, managing and sharing your testimonials in minutes, not
days. <br /> <br />
Connect your ConvertKit account to inject your testimonials into your emails.
</p>
<div slot="form">
<TertiaryButton class="w-full" on:click={completeOauth}>
<InlineLoader {loading}>Connect your account</InlineLoader>
</TertiaryButton>
<div class="text-sm mt-4 text-gray-500">
Don't have an account? <a
data-sveltekit-reload
href={signUpLink}
class="text-primary font-medium"
>
Sign up
</a>
</div>
</div>
</AuthPageLayout>
import { RequestHandler, Router } from 'express';
import { firebaseAdmin } from '../../services';
import { refreshIdToken, signInWithCustomToken } from '../utilities/sign-in-with-token';
export const convertkitRouter = Router();
const convertkitSecret = process.env.CONVERTKIT_CLIENT_SECRET as string;
// Make sure requests are coming from Convertkit.
// You'll be able to set your client_secret in the Convertkit dashboard
const verificationMiddleware: RequestHandler = (req, res, next) => {
const secret = req.body.client_secret;
if (secret !== convertkitSecret) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
};
// Create a JWT for the user and return it to Convertkit.
// Convertkit will call this endpoint with a code that you provide.
// In my case, I'm using the Firebase user's UID as the code and exchanging it for
// a custom token using the Firebase Admin SDK. Once I have the custom token, I can
// sign in the user and return the access token and refresh token to Convertkit.
convertkitRouter.post('/convertkit/auth/callback', verificationMiddleware, async (req, res) => {
const uid = req.body.code;
const token = await firebaseAdmin.auth().createCustomToken(uid);
const result = await signInWithCustomToken(token);
if (result.isErr()) {
return res.status(400).json({
error: result.error,
});
}
return res.json({
access_token: result.value.idToken,
refresh_token: result.value.refreshToken,
expires_in: result.value.expiresIn,
created_at: Date.now(),
});
});
// Convertkit will call this endpoint to refresh the access token.
convertkitRouter.post('/convertkit/auth/refresh', async (req, res) => {
const { refresh_token } = req.body;
const response = await refreshIdToken(refresh_token);
if (response.isErr()) {
return res.status(400).json({
error: response.error,
});
}
const payload = {
access_token: response.value.idToken,
refresh_token: response.value.refreshToken,
expires_in: response.value.expiresIn,
created_at: Date.now(),
};
return res.json(payload);
});
// revoke your access token here. In firebase, you'd have to log out the user
// from all sessions using the admin SDK.
convertkitRouter.post('/convertkit/auth/revoke', verificationMiddleware, async (req, res) => {
return res.json({ message: 'success' });
});
import { err, ok } from '@senja/shared/types/result';
const API_KEY = '<YOUR-FIREBASE-KEY>'
export async function signInWithCustomToken(token: string) {
const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${API_KEY}`;
const body = {
token: token,
returnSecureToken: true,
};
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
if (!response.ok) {
return err(data.error.message);
}
return ok({
idToken: data.idToken,
refreshToken: data.refreshToken,
expiresIn: parseInt(data.expiresIn),
});
}
export async function refreshIdToken(refreshToken: string) {
const url = `https://securetoken.googleapis.com/v1/token?key=${API_KEY}`;
const body = {
grant_type: 'refresh_token',
refresh_token: refreshToken,
};
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
if (!response.ok) {
return err(data.error.message);
}
const idToken = data.id_token;
const newRefreshToken = data.refresh_token;
return ok({
idToken,
refreshToken: newRefreshToken,
expiresIn: parseInt(data.expires_in),
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment