diff --git a/web/docs/guides/integrations/supertokens.mdx b/web/docs/guides/integrations/supertokens.mdx new file mode 100644 index 00000000000..872de719eb7 --- /dev/null +++ b/web/docs/guides/integrations/supertokens.mdx @@ -0,0 +1,518 @@ +--- +id: supertokens +title: 'SuperTokens' +description: "Create a Next.js application secured by Email-Password or Social Login from Stytch, and Supabase's Row Level Security (RLS)." +--- + +[SuperTokens](https://www.supertokens.com) 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 and your email 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](https://supertokens.com/docs/thirdpartyemailpassword/introduction) 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 they are authorized to do so. If you do not have a Supabase account, you will need to create one. + +> Note: You can find a completed version of this project on [Github](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-supabase) + + +## Step 1: Create a new Supabase project +From your [Supabase dashboard](https://app.supabase.io/), click `New project`. + +Enter a `Name` for your Supabase project. + +Enter a secure `Database Password`. + +Select the same `Region` you selected for your Auth0 tenant. + +Click `Create new project`. + +![New Supabase project settings](/img/guides/integrations/supertokens/supabase_dashboard_create.png) + +## Step 2: Creating tables in Supabase +From the sidebar menu in the [Supabase dashboard](https://app.supabase.io/), 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 `varchar` as primary key +- `email` as `varchar` + +Click `Save` to create the new table. + +![Users table](/img/guides/integrations/supertokens/supabase_table_create.png) + +## Step 3: Create Policies to allow Select and Insert + +Our polices will need to know who our currently logged in user is to determine whether or not they should have access. Let's create a PostgreSQL function to extract the current user from our new JWT. + +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`. + +```sql +create or replace function auth.user_id() returns text as $$ + select nullif(current_setting('request.jwt.claims', true)::json->>'userId', '')::text; +$$ language sql stable; +``` + +This will create a function called `auth.user_id()`, which will inspect the `userId` field of our JWT payload. + +Let's create our first policy that checks whether this 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](/img/guides/integrations/supertokens/create_policy.png) + +From the modal, select `Create a policy from scratch` and add the following. + +![Policy settings for SELECT](/img/guides/integrations/supertokens/policy_config_select.png) + +This policy is calling the 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`. + + +Our second policy will check whether the `user_id` being inserted is the same as the authenticated user. + +Create another policy and add the following: + +![Policy settings for INSERT](/img/guides/integrations/supertokens/policy_config_insert.png) + +Similar to the previous policy we are calling the 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 4: Setup your Next.js App with SuperTokens. + +Since the scope of this guide is limited to the intergration between SuperTokens and Supabase, you can refer to these [docs](https://supertokens.com/docs/thirdpartyemailpassword/nextjs/about) on how to setup your Next.js app with SuperTokens. + +**This demo will be using `try.supertokens.com` as our SuperTokens core instance. This is for demo and testing purposes, click [here](https://supertokens.com/pricing) to find out how you can setup your own SuperTokens core.** + +You will also need to add Supabase's config values to your `.env` file, these can be retrieved from the dashboard: + +``` +NEXT_PUBLIC_SUPABASE_URL=get-from-supabase-dashboard +NEXT_PUBLIC_SUPABASE_KEY=get-from-supabase-dashboard +SUPABASE_SIGNING_SECRET=get-from-supabase-dashboard +``` + +Once you finish setting up your app, you will be greeted with the following screen + +![SuperTokens Auth Screen](/img/guides/integrations/supertokens/supertokens_thirdpartyemailpassword_auth_screen.png) + +## Step 5: Overriding SuperTokens backend and Creating the JWT + +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, a JWT signed with Supabase's signing secret is created and attached to the user's session. Additionally their email and userId should be mapped and stored in Supabase. Attaching the JWT to the user's session will allow us to retrieve the user's email on the frontend without having to create another JWT, exposing the signing secret to the frontend. We can do so be extending SuperTokens APIs and functions using `Overriding`. + + +`Overriding` is a feature in SuperTokens which allows us to extend and modify SuperTokens's UI and Authentication experience. You can learn more about `Overriding` [here](https://supertokens.com/docs/thirdpartyemailpassword/advanced-customizations/overview). + +For our purpose we will first override SuperTokens `createNewSession` function in your backend config to create a JWT which contains the user's userId. + +```ts +// config/backendConfig.ts + +import ThirdPartyEmailPasswordNode from "supertokens-node/recipe/thirdpartyemailpassword"; +import SessionNode from "supertokens-node/recipe/session"; +import { TypeInput } from "supertokens-node/lib/build/types"; +import { appInfo } from "./appInfo"; +import jwt from "jsonwebtoken"; + +let backendConfig = (): TypeInput => { + return { + framework: "express", + supertokens: { + connectionURI: "https://try.supertokens.com", + }, + appInfo, + recipeList: [ + ThirdPartyEmailPasswordNode.init({ + providers: [ + // We have provided you with development keys which you can use for testing. + // IMPORTANT: Please replace them with your own OAuth keys for production use. + ThirdPartyEmailPasswordNode.Google({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }), + ThirdPartyEmailPasswordNode.Github({ + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + }), + ThirdPartyEmailPasswordNode.Apple({ + clientId: process.env.APPLE_CLIENT_ID, + clientSecret: { + keyId: process.env.APPLE_KEY_ID, + privateKey: process.env.APPLE_PRIVATE_KEY.replace(/\\n/g, "\n"), + teamId: process.env.APPLE_TEAM_ID, + }, + }), + ] + }), + SessionNode.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + // We want to create a JWT which contains the users userId signed with Supabase's secret so + // it can be used by Supabase to validate the user when retrieving user data from their service. + // We store this token in the accessTokenPayload so it can be accessed on the frontend and on the backend. + createNewSession: async function (input) { + const payload = { + userId: input.userId, + exp: Math.floor(Date.now() / 1000) + 60 * 60, + }; + + const supabase_jwt_token = jwt.sign(payload, process.env.SUPABASE_SIGNING_SECRET); + + input.accessTokenPayload = { + ...input.accessTokenPayload, + supabase_token: supabase_jwt_token, + }; + + return await originalImplementation.createNewSession(input); + }, + }; + }, + }, + }), + ], + isInServerlessEnv: true, + }; +}; + +``` + +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 JWT whenever the user's session is accessible. + +## Step 6: Requesting data from Supabase + +Create a new file called `utils/supabase.ts` and add the following: + +```ts +// utils/supabase.ts + +import { createClient } from "@supabase/supabase-js"; + +const getSupabase = (access_token) => { + const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_KEY); + + supabase.auth.session = () => ({ + access_token + }); + + return supabase; +}; + +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. + +## Step 7: Overriding SuperTokens Sign Up APIs: + +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. + +```ts +// config/backendConfig.ts + +import ThirdPartyEmailPasswordNode from "supertokens-node/recipe/thirdpartyemailpassword"; +import SessionNode from "supertokens-node/recipe/session"; +import { TypeInput } from "supertokens-node/lib/build/types"; +import { appInfo } from "./appInfo"; +import jwt from "jsonwebtoken"; +import { getSupabase } from "../utils/supabase"; + +let backendConfig = (): TypeInput => { + return { + framework: "express", + supertokens: { + connectionURI: "https://try.supertokens.com", + }, + appInfo, + recipeList: [ + ThirdPartyEmailPasswordNode.init({ + providers: [ + // We have provided you with development keys which you can use for testing. + // IMPORTANT: Please replace them with your own OAuth keys for production use. + ThirdPartyEmailPasswordNode.Google({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }), + ThirdPartyEmailPasswordNode.Github({ + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + }), + ThirdPartyEmailPasswordNode.Apple({ + clientId: process.env.APPLE_CLIENT_ID, + clientSecret: { + keyId: process.env.APPLE_KEY_ID, + privateKey: process.env.APPLE_PRIVATE_KEY.replace(/\\n/g, "\n"), + teamId: process.env.APPLE_TEAM_ID, + }, + }), + ], + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + thirdPartySignInUpPOST: async function (input) { + if (originalImplementation.thirdPartySignInUpPOST === undefined) { + throw Error("Should never come here"); + } + let response = await originalImplementation.thirdPartySignInUpPOST(input); + + if (response.status === "OK" && response.createdNewUser) { + // retrieve the supabase_token from the accessTokenPayload + const accessTokenPayload = response.session.getAccessTokenPayload(); + + // create a supabase client whose JWT contains the user's id + const supabase = getSupabase(accessTokenPayload.supabase_token); + // store the user's email mapped to their userId in Supabase + const { error } = await supabase + .from("users") + .insert({ email: response.user.email, user_id: response.user.id }); + + if (error !== null) { + // Since Row Level Security is enabled in our Supabase tables, if a policy for inserting + // rows to a table has not been defined, insertion will throw an error. + throw error; + } + } + + return response; + }, + + emailPasswordSignUpPOST: async function (input) { + if (originalImplementation.emailPasswordSignUpPOST === undefined) { + throw Error("Should never come here"); + } + + let response = await originalImplementation.emailPasswordSignUpPOST(input); + + if (response.status === "OK") { + // retrieve the supabase_token from the accessTokenPayload + const accessTokenPayload = response.session.getAccessTokenPayload(); + + // create a supabase client whose JWT contains the user's id + const supabase = getSupabase(accessTokenPayload.supabase_token); + + // store the user's email mapped to their userId in Supabase + const { error } = await supabase + .from("users") + .insert({ email: response.user.email, user_id: response.user.id }); + + if (error !== null) { + // Since Row Level Security is enabled in our Supabase tables, if a policy for inserting + // rows to a table has not been defined, insertion will throw an error. + throw error; + } + } + + return response; + }, + }; + }, + }, + }), + SessionNode.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + // We want to create a JWT which contains the users userId signed with Supabase's secret so + // it can be used by Supabase to validate the user when retrieving user data from their service. + // We store this token in the accessTokenPayload so it can be accessed on the frontend and on the backend. + createNewSession: async function (input) { + const payload = { + userId: input.userId, + exp: Math.floor(Date.now() / 1000) + 60 * 60, + }; + + const supabase_jwt_token = jwt.sign(payload, process.env.SUPABASE_SIGNING_SECRET); + + input.accessTokenPayload = { + ...input.accessTokenPayload, + supabase_token: supabase_jwt_token, + }; + + return await originalImplementation.createNewSession(input); + }, + }; + }, + }, + }), + ], + isInServerlessEnv: true, + }; +}; + +``` + +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 and attached to the user's session) from the user's accessTokenPayload and send a request to Supabase to insert the email-userid mapping. + +## Step 8: 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. + +```tsx +// pages/index.tsx + +import React, { useState, useEffect } from "react"; +import Head from "next/head"; +import styles from "../styles/Home.module.css"; +import ThirdPartyEmailPassword from "supertokens-auth-react/recipe/thirdpartyemailpassword"; +import dynamic from "next/dynamic"; +import { useSessionContext } from "supertokens-auth-react/recipe/session"; +import { getSupabase } from "../utils/supabase"; + +const ThirdPartyEmailPasswordAuthNoSSR = dynamic( + new Promise((res) => + res(ThirdPartyEmailPassword.ThirdPartyEmailPasswordAuth) + ), + { ssr: false } +); + +export default function Home() { + return ( + + + + ); +} + +function ProtectedPage() { + // retrieve the authenticated user's accessTokenPayload and userId from the sessionContext + const { accessTokenPayload, userId } = useSessionContext(); + + const [userEmail, setEmail] = useState(""); + useEffect(() => { + async function getUserEmail() { + // retrieve the supabase client who's JWT contains users userId, this will be + // used by supabase to check that the user can only access table entries which contain their own userId + const supabase = getSupabase(accessTokenPayload.supabase_token); + + // retrieve the user's name from the users table whose email matches the email in the JWT + const { data } = await supabase.from("users").select("email").eq("user_id", userId); + + if (data.length > 0) { + setEmail(data[0].email); + } + } + getUserEmail(); + }, []); + + async function logoutClicked() { + await ThirdPartyEmailPassword.signOut(); + ThirdPartyEmailPassword.redirectToAuth(); + } + + async function fetchUserData() { + const res = await fetch("/api/user"); + if (res.status === 200) { + const json = await res.json(); + alert(JSON.stringify(json)); + } + } + + return ( +
+ + SuperTokens 💫 + + + +
+

+ You are authenticated with SuperTokens! (UserId: {userId}) +
+ Your email retrieved from Supabase: {userEmail} +

+ +
+
+ SIGN OUT +
+
+
+
+ FETCH USER API +
+
+
+
+ ); +} + +``` + +As seen above we will be using SuperTokens `useSessionContext` hook to retrieve the authenticated user's `userId` and `accessTokenPayload`. We can then use React's `useEffect` hook to retrieve the user's email from Supabase using the JWT retrieved from the user's `accessTokenPayload` and their `userId`. + +Once you sign up you should see the following screen: + +![SuperTokens App Authenticated](/img/guides/integrations/supertokens/supabase_app_authenticated_screen.png) + +If you navigate to your table you should see a new row with the user's `user_id` and `email`. + +![Supabase Users table](/img/guides/integrations/supertokens/table_with_user.png) + +## Resources + +- [SuperTokens](https://supertokens.com/) official website. +- [SuperTokens blog](https://supertokens.com/blog). +- [SuperTokens community](https://supertokens.com/discord). +- [Auth0 documentation](https://supertokens.com/docs/guides). + + diff --git a/web/sidebars.js b/web/sidebars.js index f155b595e3d..b32e5ae3025 100755 --- a/web/sidebars.js +++ b/web/sidebars.js @@ -206,6 +206,7 @@ module.exports = { 'guides/integrations/prisma', 'guides/integrations/snaplet', 'guides/integrations/stytch', + 'guides/integrations/supertokens', 'guides/integrations/vercel', ], }, diff --git a/web/static/img/guides/integrations/supertokens/create_policy.png b/web/static/img/guides/integrations/supertokens/create_policy.png new file mode 100644 index 00000000000..377717ea4f0 Binary files /dev/null and b/web/static/img/guides/integrations/supertokens/create_policy.png differ diff --git a/web/static/img/guides/integrations/supertokens/policy_config_select.png b/web/static/img/guides/integrations/supertokens/policy_config_select.png new file mode 100644 index 00000000000..cd5a9866ffc Binary files /dev/null and b/web/static/img/guides/integrations/supertokens/policy_config_select.png differ diff --git a/web/static/img/guides/integrations/supertokens/supabase_app_authenticated_screen.png b/web/static/img/guides/integrations/supertokens/supabase_app_authenticated_screen.png new file mode 100644 index 00000000000..0be357a1276 Binary files /dev/null and b/web/static/img/guides/integrations/supertokens/supabase_app_authenticated_screen.png differ diff --git a/web/static/img/guides/integrations/supertokens/supabase_dashboard_create.png b/web/static/img/guides/integrations/supertokens/supabase_dashboard_create.png new file mode 100644 index 00000000000..a0c8c7bb1af Binary files /dev/null and b/web/static/img/guides/integrations/supertokens/supabase_dashboard_create.png differ diff --git a/web/static/img/guides/integrations/supertokens/supabase_table_create.png b/web/static/img/guides/integrations/supertokens/supabase_table_create.png new file mode 100644 index 00000000000..54f9c3f69fa Binary files /dev/null and b/web/static/img/guides/integrations/supertokens/supabase_table_create.png differ diff --git a/web/static/img/guides/integrations/supertokens/supertokens_thirdpartyemailpassword_auth_screen.png b/web/static/img/guides/integrations/supertokens/supertokens_thirdpartyemailpassword_auth_screen.png new file mode 100644 index 00000000000..df5f224e416 Binary files /dev/null and b/web/static/img/guides/integrations/supertokens/supertokens_thirdpartyemailpassword_auth_screen.png differ