NextAuth.js Flow
Documenting authorization and authentication in our platform
We don't use NextAuth!
cuHacking opts for a different authentication library (TBD), this page is for reference only.
⚙️Setup for NextAuth
Create your prisma models
We first need to create a database for NextAuth to store user and token data. NextAuth can be used without a database, but we need it in order to persist user accounts.
With these models, NextAuth automatically handles all authentication. Once a user is successfully logged in, NextAuth populates these fields to provide automatic authentication. Users can very easily be authenticated with hooks that will be explained below. The great thing with NextAuth is that we don’t need to worry about these at all.
Configuration for NextAuth
Next, we need to configure NextAuth by adding a dynamic API route handler. This route lets NextAuth automatically handle NextAuth-related requests (i.e. signIn, signOut,
callback, etc.). Our NextAuth config (authOptions
) is located in ~/server/auth.ts
. Then, we let NextAuth access authOptions
through a route.ts
file located in
~/app/api/auth/[...nextauth]/route.ts
.
NextAuth needs to use the [...nextauth]
route. Before Next.js added App Routers in 13.2, you could have your authOptions in pages/api/auth/[...nextauth].js
directly.
Since we’re using a Next.js version above 13.2, NextAuth reccommends a setup like the one I described. Their tutorial can be found
here.
Our code should look something like this:
There’s a lot more in authOptions than what you see above, but our source code should be fairly easy to read and understand with its comments.
Adding a provider
In this project, we are exclusively using providers (e.g. Google, Discord, Github, etc.) to sign in our users. We define these providers in
the providers
array in our authOptions like so:
Providers like Google require additional setup to get the client id and client secret. There should be many tutorials for other providers, but our instructions for google can be found in Contribution Guildines → Quick Start.
Wrapping components around <SessionProvider>
The last piece of setup is wrapping our components with a <SessionProvider>
.
There are two ways to retrieve a user’s session. You can do so through getServerSession(authOptions)
(or our wrapper: getServerAuthSession()
) on the server-side or useSession()
on the client side. If you want to use the useSession()
hook, then you need it to be wrapped around <SessionProvider>
. This allows instances of useSession()
to share the session
object across components and takes care of keeping the session updated and synced between tabs/windows. If you have pages that support both client and server-side rendering, then you
can pass in a session={getServerSession(authOptions)}
page prop to avoid checking the session twice.
🔒Login Flow
Creating a custom sign-in page
In the same folder where […nextauth]
is located (~/app/api/auth/
), create /signIn/page.tsx
. page.tsx
is your React sign-in page.
To make this the sign-in page used by NextAuth, add the pages
option to authOptions. Then add the signIn callback and the route that your sign-in page is located in.
It should look like this:
Using the sign-in page
Now, you can use the signIn()
method by NextAuth to send the user to your custom sign-in page. It’s not necessary, but it’s nice for creating standards.
An example of a sign-in button component can be seen below:
In the sign-in page, a button to sign-in with Google can be added like so:
🛡️Protecting Our Pages and Role-Based Authentication
In general, this works by adding a role
property (if using jwt) to our users’ tokens. We use that token to check the role of our users in our pages or in middleware.
If the role is an admin, we allow them to access the admin page. Otherwise, we redirect them to an “unauthorized” page.
Adding a role property
We first want to start by customizing what is in our users’ sessions and adding a role
property to it. We can do this through the profile()
callback in our providers
like so:
Allowing the role property to be used in our program
Now, we need to be able to use this role
property inside our program. We do this by adding the jwt()
and session()
callback into our authOptions.
Protecting our pages
Option 1: Add protection to each page
This option is very simple and can be used if we have very little pages to protect. You can simply use the getServerSession()
or useSession()
hooks inside a page to access the user’s role
. Using that, you can decide what to do based on their role. Below is a simple example of using
useSession()
to check if the user is an admin.
Option 2: Protection through middleware
This is only supported if we use the jwt
session strategy.
Create a middleware.ts
file on the root or src directory (same level as where you store your pages) to protect all pages. Adding this
file makes users require authentication. If they aren’t authenticated, it redirects them to the sign-in page by default. Below is an
example of basic middleware.ts
setup:
If we want something more advanced then just securing pages from unauthenticated users, we need to wrap the middleware with withAuth
.
Using this wrapper, we have 2 more options to protect our admin pages. (Option 1) we can use the authorized
callback, which if
false, redirects the user to the sign in page (I’m not aware if you can customize the redirection). (Option 2) we can use the
middleware function inside the wrapper and add our own custom logic. Below is an example that shows both options:
🛡️General Method for Protection and Authentication
Page Authentication
- The code example below shows a simple way to do FE authentication
- The example below takes advantage of NextJs’s server side rendering
- We get the session data on the server and then our component will have access to this session by calling useSession().
useSession() will access the props key from the return value of
getServerSideProps()
- Alternatively, we can retrieve session info on the client side by just using
useSession()
and eliminatinggetServerSideProps
Endpoint Authentication
- Create our own getServerAuthSession in
/server/auth
. This is usefull so we don’t need to import getServerSession and authOptions everytime we need to access a session on the BE side
With what we have below, all our tRPC procedures have access to our session context, allowing them to be easily authenticated!
- Create protection middleware. Middleware is something that we use to protect all of our endpoints. Essentially, all middleware will be run before calling an endpoint.
We are able to create this middleware because of the createContext function we defined earlier that adds session to our context. Now if session is null or user is null, we can assume they are unauthenticated and return an error.
- Add middleware to an endpoint:
We added protctedProcedure to the
me:
function, which means that only if a user is authenticated they will be able to access this function.
📖Sources and Further Reading
https://authjs.dev/getting-started/adapters/prisma
- Contains information about setting up Prisma to use with NextAuth
https://next-auth.js.org/configuration/providers/oauth
- Adding in an OAuth provider
https://next-auth.js.org/getting-started/client#sessionprovider
- What
<SessionProvider>
does
https://next-auth.js.org/configuration/pages
- Adding a custom sign-in page
https://authjs.dev/guides/role-based-access-control
- General role-based auth information
https://next-auth.js.org/tutorials/securing-pages-and-api-routes
- Ways to secure pages and API routes
https://next-auth.js.org/configuration/nextjs#middleware
- Setting up middleware to protect pages
https://youtu.be/MNm1XhDjX1s?si=lxIV3mX0GxLGYAyM
- A general and beginner tutorial for NextAuth
Another Method for Protection and Authentication
- From Hasith's Notion doc on auth flow, no sources for this section at the moment
Last updated on