Prepare for the PgBouncer and IPv4 deprecations on 26th January 2024

Learn more
Back
SuperTokens

SuperTokens

SuperTokens

Overview

SuperTokens is an open source authentication solution which provides many stratergies for authenticating and managing users. You can use the managed service for easy setup or you can self host the solution to have complete control over your data.

With SuperTokens, Supabase can be used to store and authorize access to user data. Supabase makes it simple to setup Row Level Security(RLS) policies which ensure users can only read and write data that belongs to them.

Documentation

SuperTokens is an open source authentication solution which provides many stratergies for authenticating and managing users. You can use the managed service for easy setup or you can self host the solution to have complete control over your data.

In this guide we will build a simple web application using SuperTokens, Supabase, and Next.js. You will be able to sign up using SuperTokens and your email and user ID will be stored in Supabase. Once authenticated the frontend will be able to query Supabase and retrieve the user's email. Our example app will be using the Email-Password and Social Login recipe for authentication and session management.

We will use Supabase to store and authorize access to user data. Supabase makes it simple to setup Row Level Security(RLS) policies which ensure users can only read and write data that belongs to them.

Demo App

You can find a demo app using SuperTokens, Supabase and Nexts.js on Github

Step 1: Create a new Supabase project

From your Supabase dashboard, click New project.

Enter a Name for your Supabase project.

Enter a secure Database Password.

Select the same Region you host your app's backend in.

Click Create new project.

New Supabase project settings

Step 2: Creating tables in Supabase

From the sidebar menu in the Supabase dashboard, click Table editor, then New table.

Enter users as the Name field.

Select Enable Row Level Security (RLS).

Remove the default columns

Create two new columns:

  • user_id as text as primary key
  • email as text

Click Save to create the new table.

Users table

Step 3: Setup your Next.js App with SuperTokens.

Since the scope of this guide is limited to the integration between SuperTokens and Supabase, you can refer to the SuperTokens website to see how to setup your Next.js app with SuperTokens.

Once you finish setting up your app, you will be greeted with the following screen

SuperTokens Auth Screen

Step 4: Creating a Supabase JWT to access Supabase

In our Nextjs app when a user signs up, we want to store the user's email in Supabase. We would then retrieve this email from Supabase and display it on our frontend.

To use the Supabase client to query the database we will need to create a JWT signed with your Supabase app's signing secret. This JWT will also need to contain the user's userId so Supabase knows an authenticated user is making the request.

To create this flow we will need to modify SuperTokens so that, when a user signs up or signs in, a JWT signed with Supabase's signing secret is created and attached to the user's session. Attaching the JWT to the user's session will allow us to retrieve the Supabase JWT on the frontend and backend (post session verification), using which we can query Supabase.

We want to create a Supabase JWT when we are creating a SuperTokens' session. This can be done by overriding the createNewSession function in your backend config.


_48
// config/backendConfig.ts
_48
_48
import ThirdPartyEmailPasswordNode from "supertokens-node/recipe/thirdpartyemailpassword";
_48
import SessionNode from "supertokens-node/recipe/session";
_48
import { TypeInput } from "supertokens-node/lib/build/types";
_48
import { appInfo } from "./appInfo";
_48
import jwt from "jsonwebtoken";
_48
_48
let backendConfig = (): TypeInput => {
_48
return {
_48
framework: "express",
_48
supertokens: {
_48
connectionURI: "https://try.supertokens.com",
_48
},
_48
appInfo,
_48
recipeList: [
_48
ThirdPartyEmailPasswordNode.init({...}),
_48
SessionNode.init({
_48
override: {
_48
functions: (originalImplementation) => {
_48
return {
_48
...originalImplementation,
_48
// We want to create a JWT which contains the users userId signed with Supabase's secret so
_48
// it can be used by Supabase to validate the user when retrieving user data from their service.
_48
// We store this token in the accessTokenPayload so it can be accessed on the frontend and on the backend.
_48
createNewSession: async function (input) {
_48
const payload = {
_48
userId: input.userId,
_48
exp: Math.floor(Date.now() / 1000) + 60 * 60,
_48
};
_48
_48
const supabase_jwt_token = jwt.sign(payload, process.env.SUPABASE_SIGNING_SECRET);
_48
_48
input.accessTokenPayload = {
_48
...input.accessTokenPayload,
_48
supabase_token: supabase_jwt_token,
_48
};
_48
_48
return await originalImplementation.createNewSession(input);
_48
},
_48
};
_48
},
_48
},
_48
}),
_48
],
_48
isInServerlessEnv: true,
_48
};
_48
};

As seen above, we will be using the jsonwebtoken library to create a JWT signed with Supabase's signing secret whose payload contains the user's userId.

We will be storing this token in the accessTokenPayload which will essentially allow us to access the supabase_token on the frontend and backend whilst the user is logged in.

Step 5: Creating a Supabase client

Create a new file called utils/supabase.ts and add the following:


_18
// utils/supabase.ts
_18
_18
import { createClient } from '@supabase/supabase-js'
_18
_18
const getSupabase = (access_token) => {
_18
const supabase = createClient(
_18
process.env.NEXT_PUBLIC_SUPABASE_URL,
_18
process.env.NEXT_PUBLIC_SUPABASE_KEY
_18
)
_18
_18
supabase.auth.session = () => ({
_18
access_token,
_18
})
_18
_18
return supabase
_18
}
_18
_18
export { getSupabase }

This will be our client for talking to Supabase. We can pass it an access_token and it will be attached to our request. This access_token is the same as the supabase_token we had created earlier.

Step 6: Inserting users into Supabase when they sign up:

In our example app there are two ways for signing up a user. Email-Password and Social Login based authentication. We will need to override both these APIs such that when a user signs up, their email mapped to their userId is stored in Supabase.


_92
// config/backendConfig.ts
_92
_92
import ThirdPartyEmailPasswordNode from "supertokens-node/recipe/thirdpartyemailpassword";
_92
import SessionNode from "supertokens-node/recipe/session";
_92
import { TypeInput } from "supertokens-node/lib/build/types";
_92
import { appInfo } from "./appInfo";
_92
import jwt from "jsonwebtoken";
_92
import { getSupabase } from "../utils/supabase";
_92
_92
let backendConfig = (): TypeInput => {
_92
return {
_92
framework: "express",
_92
supertokens: {
_92
connectionURI: "https://try.supertokens.com",
_92
},
_92
appInfo,
_92
recipeList: [
_92
ThirdPartyEmailPasswordNode.init({
_92
providers: [...],
_92
override: {
_92
apis: (originalImplementation) => {
_92
return {
_92
...originalImplementation,
_92
// the thirdPartySignInUpPost function handles sign up/in via Social login
_92
thirdPartySignInUpPOST: async function (input) {
_92
if (originalImplementation.thirdPartySignInUpPOST === undefined) {
_92
throw Error("Should never come here");
_92
}
_92
_92
// call the sign up/in api for social login
_92
let response = await originalImplementation.thirdPartySignInUpPOST(input);
_92
_92
// check that there is no issue with sign up and that a new user is created
_92
if (response.status === "OK" && response.createdNewUser) {
_92
_92
// retrieve the accessTokenPayload from the user's session
_92
const accessTokenPayload = response.session.getAccessTokenPayload();
_92
_92
// create a supabase client with the supabase_token from the accessTokenPayload
_92
const supabase = getSupabase(accessTokenPayload.supabase_token);
_92
_92
// store the user's email mapped to their userId in Supabase
_92
const { error } = await supabase
_92
.from("users")
_92
.insert({ email: response.user.email, user_id: response.user.id });
_92
_92
if (error !== null) {
_92
_92
throw error;
_92
}
_92
}
_92
_92
return response;
_92
},
_92
// the emailPasswordSignUpPOST function handles sign up via Email-Password
_92
emailPasswordSignUpPOST: async function (input) {
_92
if (originalImplementation.emailPasswordSignUpPOST === undefined) {
_92
throw Error("Should never come here");
_92
}
_92
_92
let response = await originalImplementation.emailPasswordSignUpPOST(input);
_92
_92
if (response.status === "OK") {
_92
_92
// retrieve the accessTokenPayload from the user's session
_92
const accessTokenPayload = response.session.getAccessTokenPayload();
_92
_92
// create a supabase client with the supabase_token from the accessTokenPayload
_92
const supabase = getSupabase(accessTokenPayload.supabase_token);
_92
_92
// store the user's email mapped to their userId in Supabase
_92
const { error } = await supabase
_92
.from("users")
_92
.insert({ email: response.user.email, user_id: response.user.id });
_92
_92
if (error !== null) {
_92
_92
throw error;
_92
}
_92
}
_92
_92
return response;
_92
},
_92
};
_92
},
_92
},
_92
}),
_92
SessionNode.init({...}),
_92
],
_92
isInServerlessEnv: true,
_92
};
_92
};

As seen above, we will be overriding the emailPasswordSignUpPOST and thirdPartySignInUpPOST APIs such that if a user signs up, we retrieve the Supabase JWT (which we created in the createNewSession function) from the user's accessTokenPayload and send a request to Supabase to insert the email-userid mapping.

Step 7: Retrieving the user's email on the frontend

Now that our backend is setup we can modify our frontend to retrieve the user's email from Supabase.


_65
// pages/index.tsx
_65
_65
import React, { useState, useEffect } from 'react'
_65
import Head from 'next/head'
_65
import styles from '../styles/Home.module.css'
_65
import ThirdPartyEmailPassword, {
_65
ThirdPartyEmailPasswordAuth,
_65
} from 'supertokens-auth-react/recipe/thirdpartyemailpassword'
_65
import dynamic from 'next/dynamic'
_65
import { useSessionContext } from 'supertokens-auth-react/recipe/session'
_65
import { getSupabase } from '../utils/supabase'
_65
_65
export default function Home() {
_65
return (
_65
// We will wrap the ProtectedPage component with ThirdPartyEmailPasswordAuth so only an
_65
// authenticated user can access it. This will also allow us to access the users session information
_65
// within the component.
_65
<ThirdPartyEmailPasswordAuth>
_65
<ProtectedPage />
_65
</ThirdPartyEmailPasswordAuth>
_65
)
_65
}
_65
_65
function ProtectedPage() {
_65
// retrieve the authenticated user's accessTokenPayload and userId from the sessionContext
_65
const { accessTokenPayload, userId } = useSessionContext()
_65
_65
if (sessionContext.loading === true) {
_65
return null
_65
}
_65
_65
const [userEmail, setEmail] = useState('')
_65
useEffect(() => {
_65
async function getUserEmail() {
_65
// retrieve the supabase client who's JWT contains users userId, this will be
_65
// used by supabase to check that the user can only access table entries which contain their own userId
_65
const supabase = getSupabase(accessTokenPayload.supabase_token)
_65
_65
// retrieve the user's name from the users table whose email matches the email in the JWT
_65
const { data } = await supabase.from('users').select('email').eq('user_id', userId)
_65
_65
if (data.length > 0) {
_65
setEmail(data[0].email)
_65
}
_65
}
_65
getUserEmail()
_65
}, [])
_65
_65
return (
_65
<div className={styles.container}>
_65
<Head>
_65
<title>SuperTokens 💫</title>
_65
<link rel="icon" href="/favicon.ico" />
_65
</Head>
_65
_65
<main className={styles.main}>
_65
<p className={styles.description}>
_65
You are authenticated with SuperTokens! (UserId: {userId})
_65
<br />
_65
Your email retrieved from Supabase: {userEmail}
_65
</p>
_65
</main>
_65
</div>
_65
)
_65
}

As seen above we will be using SuperTokens useSessionContext hook to retrieve the authenticated user's userId and accessTokenPayload. Using React's useEffect hook we can use the Supabase client to retrieve the user's email from Supabase using the JWT retrieved from the user's accessTokenPayload and their userId.

Step 8: Create Policies to enforce Row Level Security for Select and Insert requests

To enforce Row Level Security for the Users table we will need to create policies for Select and Insert requests.

These polices will retrieve the userId from the JWT and check if it matches the userId in the Supabase table

To do this we will need a PostgreSQL function to extract the userId from the JWT.

The payload in the JWT will have the following structure:


_10
// JWT payload
_10
{
_10
userId,
_10
exp
_10
}

To create the PostgreSQL function, lets navigate back to the Supabase dashboard, select SQL from the sidebar menu, and click New query. This will create a new query called new sql snippet, which will allow us to run any SQL against our Postgres database.

Write the following and click Run.


_10
create or replace function auth.user_id() returns text as $$
_10
select nullif(current_setting('request.jwt.claims', true)::json->>'userId', '')::text;
_10
$$ language sql stable;

This will create a function called auth.user_id(), which will inspect the userId field of our JWT payload.

SELECT query policy

Our first policy will check whether the user is the owner of the email.

Select Authentication from the Supabase sidebar menu, click Policies, and then New Policy on the Users table.

Create new policy

From the modal, select Create a policy from scratch and add the following.

Policy settings for SELECT

This policy is calling the PostgreSQL function we just created to get the currently logged in user's ID auth.user_id() and checking whether this matches the user_id column for the current email. If it does, then it will allow the user to select it, otherwise it will continue to deny.

Click Review and then Save policy.

INSERT query policy

Our second policy will check whether the user_id being inserted is the same as the userId in the JWT.

Create another policy and add the following:

Policy settings for INSERT

Similar to the previous policy we are calling the PostgreSQL function we created to get the currently logged in user's ID auth.user_id() and check whether this matches the user_id column for the row we are trying to insert. If it does, then it will allow the user to insert the row, otherwise it will continue to deny.

Click Review and then Save policy.

Step 9: Test your changes

You can now sign up and you should see the following screen:

SuperTokens App Authenticated

If you navigate to your table you should see a new row with the user's user_id and email.

Supabase Users table

Resources

Details

DeveloperSuperTokens
CategoryAuth