If you want to integrate "Sign in with Google" in your Next.js app using Supabase Auth, you're in the right place. This comprehensive guide covers everything you need, from creating a Supabase project and configuring Google Cloud for OAuth to setting up your Next.js project and implementing robust authentication and authorization logic.
In this guide, you'll learn how to:
- Set up a Supabase project and configure Google Cloud for OAuth
- Implement Google Sign-in in your Next.js application
- Access user information inside both client and server components
- Protect routes using middleware
- Assign roles to users using Supabase auth hooks
- Create and manage a users table in the public schema
Let's get started!
Create a Supabase Project
-
Go to supabase.com/dashboard/projects (sign in if you have an account, or create one if you don't).
-
Click on New project.
-
Give your project a name, choose a database password, select a region, and then click on Create new project.
-
The project setup may take a minute or two to complete. Once the setup is finished, scroll down the page to find the Project API and JWT Settings sections.
From the Project API section, copy the Project URL and API Key. From the JWT Settings section, copy the JWT Secret. Save these three values somewhere safe - we'll need them later in our setup process.
You'll notice that the API Key is tagged with anon and public. This anon key (short for anonymous) is a public key that's safe to use in a browser, but only if you've enabled RLS (row-level security) for your tables and configured appropriate policies.
Note
When you create a Supabase project, Supabase automatically deploys an instance of the Auth API server and injects your database with the required auth schema. This Auth API server is responsible for:
- Validating, issuing, and refreshing JSON Web Tokens (JWTs).
- Serving as an intermediary between your app and the auth information in the database.
- Communicating with external providers for Social Login and SSO (Single Sign-On).
To view the auth schema in the Supabase dashboard:
- Click on Database in the sidebar navigation.
- You'll see a dropdown menu to select a schema. Choose auth from this dropdown.
- You'll notice a list of tables appears. These are the tables that store user information, sessions, identities and other authentication-related data
It's crucial to understand that the auth schema in Supabase is managed internally and is read-only through the dashboard. This means that your application code cannot directly access or modify the auth schema tables.
But what if you want to keep track of user information, for example, to display a list of users on a page accessible only to administrators? In such cases, you must create your own user table in the public schema, which I'll guide you through later in this guide.
You can then link the public.users table to the auth.users table using the users' UUIDs as foreign keys. This approach allows you to store and manage user data while maintaining the integrity and security of Supabase's core authentication system.
Configure Google Cloud for OAuth
- Go to the Google Cloud Console.
- On the main navigation bar at the top, to the left of the search input, there is a dropdown menu. Click on the dropdown menu to open a pop-up window with all of your projects listed. In the pop-up window, click on NEW PROJECT.
- Enter a project name and click CREATE. It’ll take half a minute or so to create the project. Once the project is created, select it.
- Navigate to APIs & Services > OAuth consent screen.
OAuth consent screen
-
Choose External as the user type (this will make your project accessible to anyone with a Google account) and click CREATE.
-
Under App information, enter the App name and the user support email. Under Authorized domains, enter the Supabase Project URL (which you had copied earlier). Under Developer contact information, enter your email address. Then click SAVE AND CONTINUE.
An authorized domain is a domain that you explicitly allow to use your OAuth credentials. By entering the Supabase Project URL as an authorized domain, you ensure that only requests originating from the Supabase Project URL can use your OAuth Client ID and Client Secret to authenticate users. This helps prevent unauthorized use of your OAuth credentials.
Scopes
On the Scopes page, you'll see the following information:
- Non-sensitive scopes: Permissions that your app requests which are generally safe and don't require extensive review by Google.
- Sensitive scopes: Permissions that request access to more private user data.
- Restricted scopes: Permissions that request access to highly sensitive user data.
If your app doesn't require access to sensitive or restricted data, you can proceed without adding any scopes. Click SAVE AND CONTINUE.
Test users
On the Test users page, you'll see the following information:
- Test users: While publishing status is set to Testing, only test users are able to access the app. Allowed user cap prior to app verification is 100, and is counted over the entire lifetime of the app.
If you don't need to add any specific test users at this point, you can proceed by clicking SAVE AND CONTINUE.
Summary
On the Summary page, review the information to ensure everything is correct. Click BACK TO DASHBOARD.
OAuth Client ID
Navigate to APIs & Services > Credentials. Click on Create Credentials and select OAuth client ID.
On the Create OAuth Client ID page, follow the steps below to fill out the fields:
- Application Type: Select Web application.
- Name: Enter a name for your OAuth 2.0 client. This name is only used to identify the client in the console and will not be shown to end users.
- Authorized JavaScript origins: Add the origins from which your app is allowed to access the Google APIs. As we are developing locally, add
http://localhost:3000
. - Authorized Redirect URIs: Add the Callback URL from your Supabase dashboard.
- Click CREATE to generate your Client ID and Client Secret. Copy both the Client ID and the Client Secret and save them somewhere safe; we will need them in the next step.
How to locate the Callback URL (OAuth) in Supabase dashboard
To find the Callback URL in your Supabase dashboard, follow these steps:
- On the sidebar navigation, navigate to Authentication.
- Click Providers under the CONFIGURATION section.
- Click Google in the list of providers. Scroll down slightly, and you'll see the Callback URL displayed.
Next, return to your Supabase dashboard to continue the setup.
Enable Google sign-in
- On the sidebar navigation, navigate to Authentication.
- Click on Providers under the CONFIGURATION section.
- Find Google in the list and click on it.
- Toggle Enable Sign In with Google to on.
- Enter the Client ID and Client Secret you obtained from Google Cloud.
- Click Save to apply the changes
Add Redirect URL
- On the sidebar navigation, navigate to Authentication.
- Click on URL Configuration under the CONFIGURATION section.
- In the Redirect URLs section, add
http://localhost:3000/**
.
By adding http://localhost:3000/**
, you ensure that after a user has authenticated using Google sign-In, they are redirected back to your local development server (running at http://localhost:3000).
Set up a Next.js Project
Create a Next.js project by using create-next-app
:
Note: I’ve named the project supabase-auth-nextjs-demo
, but you can choose any name you prefer.
Once the project is created, navigate to the project directory.
Install packages
We'll need to install several packages for our project. Let's go through them:
Install shadcn/ui:
shadcn/ui is a collection of re-usable components that we'll use in our project. To set it up, run the following command:
Follow the prompts to configure shadcn/ui according to your preferences.
We'll be using the Button
, Dropdown
, and Toast
components. Install them with these commands:
Install Supabase packages
We'll need the Supabase client library to interact with our Supabase backend:
Install client-only and server-only packages
These packages are useful for ensuring that certain modules are only used in their intended environment, client or server. For example, if you try to use a module marked as client-only
in a server environment, you'll get an error, and vice versa.
Install JWT decoding libraries
These libraries are necessary to decode the JWT and access the user role. We'll be using Supabase auth hooks to add user roles inside the JWT, so we need libraries to decode them. jose
is used for server-side decoding, while jwt-decode
is used for client-side decoding.
Install icon libraries
Configure Environment Variables
Create a .env.local
file in the root directory of your project, and add the following environment variables:
Replace your_supabase_project_url
, your_supabase_anon_key
, and your_supabase_jwt_secret
with the values you copied earlier from your Supabase project.
If you need to find these values again, click Project Settings in the sidebar navigation of your Supabase dashboard. Under the CONFIGURATION section, click API. Here you'll find the Project URL, the API Key and the JWT Secret.
Set up Supabase clients
We need to set up two different Supabase clients.
Create a new folder named supabase
inside the lib
folder, and inside supabase
, create two files: client.ts
and server.ts
.
In client.ts
, add the following code:
In server.ts
, add the following code:
You might be wondering why we need two separate clients for Supabase Auth. The reason is that we use different methods to access Supabase depending on where we're making the call:
- The
createClient
function inclient.ts
is used to access Supabase inside client components. - The
createClient
function inserver.ts
is used to access Supabase inside server components, server actions, and route handlers.
Set up middleware
Middleware in Next.js allows you to run code before a request is completed. In our case, we'll use it to handle authentication and authorization. Let's set it up:
Root middleware
First, create a middleware.ts
file at the root of your project:
The matcher
array specifies which routes the middleware should run on. In this case:
- /protected: Ensures only authenticated users can access this route
- /signin: Handles redirects for already authenticated users
- /admin/:path*: Protects all routes under
/admin
, ensuring only users withadmin
roles can access them
Supabase middleware
Now, create a middleware.ts
file inside the lib/supabase
folder:
This middleware file handles several key aspects of our authentication flow:
- It creates a Supabase client.
- It checks the user's authentication status and role.
- It manages redirects based on the user's authentication status and the route they're trying to access.
Here's a breakdown of the redirect logic:
- If an unauthenticated user tries to access any page mentioned in the
matcher
array, they are redirected to the sign-in page. While redirecting, we also set thenext
parameter. Thisnext
parameter stores the pathname of the page the user was trying to access. After successful authentication, we use this parameter to redirect the user back to their intended destination. - If a signed-in user tries to access the sign-in page, we redirect them to the home page. This is because the user is already authenticated, so they don't need to go to the sign-in page anymore.
- Finally, if a signed-in user tries to access an admin page but does not have the
admin
role, they are redirected to the home page. This ensures that only users with the appropriate permissions can access sensitive admin areas of the application.
By implementing these redirects, we maintain proper access control throughout the application, ensuring users are always directed to the appropriate pages based on their authentication status and roles.
Notice that in the middleware, we use the getUserRole()
function to determine the user's role. Let's create this function.
Create a new file named get-user-role.ts
inside the lib
folder:
This function does the following:
- It creates a Supabase client.
- It retrieves the current session using
supabase.auth.getSession()
. - If a session exists, it extracts the access token (JWT).
- It then prepares to verify the JWT token. The JWT secret is encoded using
TextEncoder().encode(process.env.SUPABASE_JWT_SECRET)
. This step is necessary because thejwtVerify
function from thejose
library expects the secret to be in a specific format - aUint8Array
. - It verifies the JWT token using the encoded
SUPABASE_JWT_SECRET
. - After verification, it extracts the
role
from theapp_metadata
in the token payload. - Finally, it returns the user's role.
Assign roles to users
After setting up our middleware, we need to address how roles are assigned to users. This is where the concept of custom claims comes into play.
Custom claims are essentially special attributes that we can attach to a user. In our case, we want to attach a role, so that we can use this role to control access to special areas of our application.
These custom claims are implemented using a Custom Access Token Auth Hook. Auth hooks are Postgres functions that we can use to alter the default Supabase Auth flow. We'll use the following function to add a role field to the JWT:
This function does the following:
- It checks the user's email address.
- If the email is
rawgrittt@gmail.com
, it sets the role toadmin
. - For all other email addresses, it sets the role to
regular
. - The role is set inside the
app_metadata
object, which is part of the JWT claims.
We've used RAISE LOG
statements throughout the function. These are helpful for debugging purposes, allowing you to study the structure of the event object passed to the function and troubleshoot if any errors occur.
To find these logs in the Supabase dashboard, navigate to Logs on the sidebar navigation and then click Postgres under the INFRASTRUCTURE LOGS section.
Notice the last three lines of code in the function above. These lines are crucial for setting up the correct permissions for our function:
- We grant the
supabase_auth_admin
role permission to use thepublic
schema. - We grant the
supabase_auth_admin
role permission to execute ourset_user_role
function. - We revoke execute permissions from the
authenticated
,anon
, andpublic
roles.
The reason for revoking permissions is to enhance security. By default, the public
role has access to functions created in the public
schema, and both anon
and authenticated
roles inherit permissions from the public
role. By revoking these permissions, we ensure that our role-setting function is not accessible via Supabase Serverless APIs or by regular users. This prevents potential misuse or unauthorized manipulation of user roles.
To implement this function:
- Navigate to SQL Editor in the sidebar navigation of the Supabase dashboard.
- Paste the entire function (including the permission grants and revokes) into the editor.
- Click Run to create the function and set the permissions.
Finally, to enable the custom claims:
- In the Supabase dashboard, navigate to Authentication in the sidebar navigation.
- Click Hooks under the CONFIGURATION section.
- Enable Custom Access token (JWT) Claims hook.
- Select the
set_user_role
function.
By setting up this hook, Supabase will add the role
property to the JWT before releasing it, ensuring that each user's token contains their assigned role.
Create a useUser custom hook
Now, we will create a custom hook that we will use inside client components to get user details. At the root of the project, create a directory named hooks
and inside hooks
, create a file named use-user.ts
:
Create a users Table
In most applications, you'll want to access user data for various purposes. Even though we've taken care of authentication and authorization, we haven't yet saved the users' data in a way that's easily accessible to our application.
As discussed earlier, the user data inside the auth.users
table is not directly accessible to our application. To solve this, we need to create our own table in the public
schema.
In the Supabase dashboard, navigate to SQL Editor on the sidebar navigation. Then paste the following function in the editor and click Run.
This will creates a table named users
in the public
schema with four columns:
- id: A
UUID
that serves as the primary key and references theid
column in theauth.users
table. - name: The user's full name.
- email: The user's email address (must be unique).
- role: The user's role in the application (defaults to 'regular').
The REFERENCES auth.users(id)
clause in the id
column definition is important to understand. It sets up a foreign key relationship between our public.users
table and the auth.users
table. This relationship ensures that every id
in our public.users
table corresponds to a valid user in the auth.users
table.
Now that we have created the table, we'll use a concept called database triggers to automatically populate data inside the users
table in the public
schema.
What are database triggers?
Database triggers are special functions that automatically run when certain events occur in a database. Triggers can be set to run either before or after the triggering event.
In the SQL Editor, paste the code below and click Run to create the trigger:
Let's break down what this code does:
- We create a function called
create_new_user()
that will be executed by our trigger. - Inside this function, we insert a new row into our
public.users
table using data from the newly insertedauth.users
row (represented by NEW). - We use
NEW.raw_user_meta_data->>'full_name'
to extract the full name from the metadata. - We set the
role
toadmin
for a specific email address (in this case,rawgrittt@gmail.com
) andregular
for all others. - We create a trigger named
on_auth_user_created
that executes our function after each insert on theauth.users
table.
With this trigger in place, every time a new user signs up or is created in the auth.users
table, a corresponding entry will automatically be inserted into our public.users
table.
This ensures that our application always has access to the latest user data in a format that we can easily query and manipulate.
Enable RLS on the users table
Note
Every table created in the public schema should have Row Level Security (RLS) enabled.
RLS allows you to control which users can access and modify specific rows in a table based on their authentication status and/or role or other criteria. Since the users
table contains sensitive information, we'll enable RLS and create a policy that only allows logged-in users with an admin
role to query and manipulate its data.
In the Supabase dashboard, navigate to the SQL Editor on the sidebar navigation, and run the following command:
Next, create the policy by running this command:
Let's break down each part of this policy:
-
CREATE POLICY: Creates a policy named
Allow only admin access to the users table
. -
ON public.users: Specifies that the policy applies to the
users
table within thepublic
schema. -
FOR ALL: Means this policy applies to all operations on the table, including
SELECT
,INSERT
,UPDATE
, andDELETE
. -
USING Clause: Defines the actual condition that must be satisfied for a row to be accessible.
current_setting('request.jwt.claims', true)
fetches the current JWT claims from the session settings. Thetrue
parameter indicates that if the setting is not found, it should returnNULL
instead of throwing an error.::jsonb
casts the fetched JWT claims to a JSONB (Binary JSON) data type.#>> '{app_metadata, role}'
operator extracts the value from the JSONB data at the specified path. In this case, it navigates to theapp_metadata
section and then to therole
key within the JWT claims. -
= 'admin'
checks if the extractedrole
is equal toadmin
. If it is, the condition is satisfied, and the user is granted access to the row.
The policy essentially ensures that only users whose JWT claims include a role of admin
can perform any operation on the users
table.
To verify that the RLS policy works as expected, sign in using a non-admin email (after you have created all the components and pages). Then query the users
table from any page except the admin
page. You'll receive an empty array in response.
Create components
In a typical app, you'll find a main navigation bar with navigation links and a sign-in button. This is the structure we're going to follow.
We'll create a main navigation bar component with the following links:
- Home
- Client
- Server
- Protected
- Admin
These links are carefully planned to cover common authentication and authorization use cases in a Next.js app:
- Accessing signed-in user information in client components
- Accessing signed-in user information in server components
- Protecting routes accessible only to signed-in users
- Protecting routes accessible only to signed-in users with specific roles (e.g., admin)
Let's start by defining our components.
MainNav component
Create a file named main-nav.tsx
in the components
folder:
NavItem component
Create a file named nav-item.tsx
in the components
folder:
UserAccountNav component
Create a file named user-account-nav.tsx
in the components
folder:
Here, we check for the presence of a user. If no user is present, we render a SignInButton
component. If a user is present, we render a user icon. Clicking on the user icon reveals a dropdown with the user's email and a Sign out button.
Note that the getUser()
method sends a request to the Supabase Auth server every time to revalidate the Auth token.
SignInButton component
Create a file named signin-button.tsx
in the components
folder:
SignOutButton component
Create a file named signout-button.tsx
in the components
folder:
In the SignOutButton
component, we use the signOut()
method to sign out the user and then redirect them to the signin
page. We also refresh the page to ensure that the UserAccountNav
component (which is a server component) accurately renders the authentication state.
Icons component
Create a file named icons.tsx
in the components
folder:
Now that we've created our main navigation component, we need it to appear on every page of our application. To achieve this, we'll import and render it inside the root layout file.
We'll also render the Toaster
component here so that toast notifications can be triggered from any page or component within our app.
Create pages
Now that we have our core components ready, let's create the pages that will be rendered when we click on the links in the main navigation.
Home page
Replace the code inside the app/page.tsx
file with the code below:
Client page
Inside the app
folder, create a folder named client
and inside client
, create a file named page.tsx
:
Server page
Inside the app
folder, create a folder named server
and inside server
, create a file named page.tsx
:
Protected page
Inside the app
folder, create a folder named protected
and inside protected
, create a file named page.tsx
:
Admin page
Inside the app
folder, create a folder named admin
and inside admin
, create a file named page.tsx
:
SignIn page
Inside the app
folder, create a folder named signin
and inside signin
, create a file named page.tsx
:
When a user clicks the Sign in with Google
button, the following sequence of events occurs:
- The
signInWithGoogle
function is called, initiating the OAuth flow with Google. - The user is redirected to Google's authentication page, where they sign in and grant necessary permissions.
- After successful authentication, Google redirects the user back to our application's
/auth/callback
route. Notice how we include thenext
parameter in this redirect URL, ensuring we don't lose context of where the user originally intended to go.
Set up Auth callback route handler
Inside the app
folder, create a folder named auth
and inside auth
, create a folder naned callback
and inside callback
, create a file named route.ts
:
After Google authenticates the user and redirects them back to our application's /auth/callback
route, the following process occurs:
- The
GET
function handles the incoming request to the/auth/callback
route. - We extract the
code
(authorization code) sent by Google. - We also retrieve the
next
parameter, which contains the path where the user should be redirected after successful authentication. If it's not present, we default to the root path ("/"). - If an authorization code is present, we create a Supabase client.
- We then use Supabase's
exchangeCodeForSession
method to exchange the authorization code for a user session. This step completes the OAuth flow, establishing the user's authenticated session in our application. - If the code exchange is successful (no error), we redirect the user to the path specified in the
next
parameter, or the home page if no specific path was provided. - If there's no authorization code, or if an error occurs during the code exchange, we redirect the user to an error page.
This callback handler ensures that after Google's authentication, we complete the process by establishing a session in our application and then direct the user to their intended destination.
Conclusion
Our implementation of Google sign-in using Next.js and Supabase Auth is complete. Now it's time to put your work to the test:
- Click through the links in the navigation bar to explore all the functionality you've implemented.
- Try signing in and out to ensure the authentication flow works smoothly.
- Check if the user information is correctly displayed and updated across different pages.
If you have ideas on how to improve this implementation, spot any mistakes, or have any questions, please don't hesitate to leave a comment below.
You can find the complete source code for this project on GitHub.
Last updated on Monday, July 22nd, 2024