DEV Community

Rafael Magalhaes
Rafael Magalhaes

Posted on • Originally published at blog.rrrm.co.uk on

Authentication in Nextjs

Most developers are familiar with the popular NextAuth.js plugin for handling authentication in Next.js applications. It's a powerful and easy-to-use tool that simplifies the process of adding authentication to your project. However, some developers prefer to avoid using third-party plugins and instead implement authentication themselves using custom code.

In this context, it's worth mentioning that Next.js provides various tools and features for handling authentication without relying on external plugins. One of these tools is middleware, which is a function that runs before your page component is rendered and can be used to implement authentication logic.

Recently, I wrote a blog about implementing authentication in Nuxt 3 using middleware, and I wanted to see if it was possible to achieve the same results in Next.js. In this blog, I will be converting my Nuxt 3 project into a Next.js project and exploring how to use middleware to handle authentication. By the end of this blog, you will have a clear understanding of how to implement authentication in Next.js without relying on third-party plugins.

lets start by creating a blank nextjs project

npx create-next-app@latest

select your options I just used typescript as default

install zustand

npm i zustand

I choose zustand as my state management library because it looks easy and straight forward.

exactly like the tutorial in my Nuxt 3 post, I will be using DummyJSON

store

path: store/useAuthStore.ts

 // Importing create function from the Zustand library import { create } from 'zustand' // Defining an interface for the store's state interface AuthStoreInterface { authenticated: boolean // a boolean value indicating whether the user is authenticated or not setAuthentication: (val: boolean) => void // a function to set the authentication status user: any // an object that stores user information setUser: (user: any) => void // a function to set user information } // create our store export const useAuthStore = create<AuthStoreInterface>((set) => ({ authenticated: false, // initial value of authenticated property user: {}, // initial value of user property setAuthentication: (val) => set((state) => ({ authenticated: val })), // function to set the authentication status setUser: (user) => set({ user }), // function to set user information })) 
Enter fullscreen mode Exit fullscreen mode

Overall, this code creates a store for authentication-related state in a React application using Zustand. It provides methods to set and update the authentication and user information stored in the state.

Layout

I also wanted to play around with the new layouts feature in next so I created a layout folder

path: layouts/DefaultLayouts/index.tsx

// Importing necessary components and functions import Navbar from '~/components/Navbar' // a component for the website navigation bar import Footer from '~/components/Footer' // a component for the website footer import { useEffect } from 'react' // importing useEffect hook from react import { getCookie } from 'cookies-next' // a function to get the value of a cookie import { useAuthStore } from '~/store/useAuthStore' // a hook to access the authentication store // Defining the layout component export default function Layout({ children }: any) { // Getting the token value from a cookie const token = getCookie('token') // Getting the setAuthentication function from the authentication store const setAuthentication = useAuthStore((state) => state.setAuthentication) // Running a side effect whenever the token value changes useEffect(() => { console.log(token) // Logging the token value for debugging purposes if (token) { setAuthentication(true) // Setting the authentication status to true if a token exists } }, [token]) // Rendering the layout with the Navbar, main content, and Footer components return ( <> <Navbar /> <main className="mainContent">{children}</main>  <Footer /> </>  ) } 
Enter fullscreen mode Exit fullscreen mode

Pretty straight forward we have our Navbar and Footer component and we wrap the main container between both, we are also calling the store to check if token is there and setting the authentication state

Now just need to use our layout in the _app.tsx file

import '../styles/globals.scss' import Layout from '~/layouts/DefaultLayout' import { AppProps } from 'next/app' export default function App({ Component, pageProps }: AppProps) { return ( <Layout> <Component {...pageProps} /> </Layout> ) } 
Enter fullscreen mode Exit fullscreen mode

middleware

the code below defines a middleware function to handle user authentication in Next.js. The function checks whether a user has a token (stored in a cookie) to access protected routes. If the user does not have a token and the requested path is not allowed, the middleware will redirect them to the signin page. If the user is already authenticated and tries to access a path that is allowed, the middleware will redirect them to the home page. This function also ignores any routes that start with /api and /_next to avoid running the middleware multiple times.

 import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export function middleware(request: NextRequest) { const { pathname } = request.nextUrl /* ignore routes starting with api and _next (temp solution) matchers in next.config isn't working without this the middleware will run more than once so to avoid this we will ignore all paths with /api and /_next */ if ( request.nextUrl.pathname.startsWith('/api/') || request.nextUrl.pathname.startsWith('/_next/') ) { return NextResponse.next() } // our logic starts from here let token = request.cookies.get('token')?.value // retrieve the token const allowedRoutes = ['/auth/signin', '/auth/register'] // list of allowed paths user can visit without the token const isRouteAllowed = allowedRoutes.some((prefix) => pathname.startsWith(prefix)) // check path and see if matches our list then return a boolean // redirect to login if no token if (!token) { if (isRouteAllowed) { // check if path is allowed return NextResponse.next() } // if path is not allowed redirect to signin page return NextResponse.redirect(new URL('/auth/signin', request.url)) } //redirect to home page if logged in if (isRouteAllowed && token) { return NextResponse.redirect(new URL('/', request.url)) } } 
Enter fullscreen mode Exit fullscreen mode

Pages

The index page

import Head from 'next/head' export default function Home() { return ( <div> <h1>Homepage</h1>  </div>  ) } 
Enter fullscreen mode Exit fullscreen mode

The SignIn page

 import { NextPage } from 'next' import { useState } from 'react' import { setCookie } from 'cookies-next' import { useRouter } from 'next/router' import { useAuthStore } from '~/store/useAuthStore' // import our useAuthStore const SignIn: NextPage = (props) => { // set UserInfo state with inital values const [userInfo] = useState({ email: 'kminchelle', password: '0lelplR' }) const router = useRouter() // import state from AuthStore const setUser = useAuthStore((state) => state.setUser) const setAuthentication = useAuthStore((state) => state.setAuthentication) const login = async () => { // do a post call to the auth endpoint const res = await fetch('https://dummyjson.com/auth/login', { method: 'post', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: userInfo.email, password: userInfo.password, }), }) // check if response was ok if (!res.ok) { return console.error(res) } // retrieve data from the response const data = await res.json() // check if we have data if (data) { setUser(data) // set data to our user state setAuthentication(true) // set our authentication state to true setCookie('token', data?.token) // set token to the cookie router.push('/') // redirect to home page } } return ( <div> <div className="title"> <h2>Login</h2>  </div>  <div className="container form"> <label> <b>Username</b>  </label>  <input type="text" className="input" placeholder="Enter Username" name="uname" value={userInfo.email} onChange={(event) => (userInfo.email = event.target.value)} required /> <label> <b>Password</b>  </label>  <input type="password" className="input" placeholder="Enter Password" value={userInfo.password} onChange={(event) => (userInfo.password = event.target.value)} name="psw" required /> <button onClick={login} className="button"> Login </button>  </div>  </div>  ) } export default SignIn 
Enter fullscreen mode Exit fullscreen mode

The code provided is sufficient to implement authentication in Next.js without using the NextAuth plugin. However, it should be noted that this code only supports email and password authentication, and not Single Sign-On (SSO) options like Google or GitHub. During a project that involved converting a Nuxt 3 project to a Next.js project, I found Zustand to be a useful tool. In comparison to Redux and Context, Zustand is lightweight and more preferable.

The middleware function in Next.js is a valuable addition. However, I did encounter some issues with the matchers while using it, but was able to find workarounds to solve the problems.

Full project layout

components/ - Footer - Navbar layouts/ - DefaultLayout pages/ - auth/ - login.tsx <-- login page - register.tsx <-- /register page - index.tsx <- homepage - _app.tsx <- nextjs app file store/ - useAuthStore.ts <- zustand store styles/ - globals.scss <- global styleguide middleware.ts <- middleware file in root of project 
Enter fullscreen mode Exit fullscreen mode

Preview: https://next-auth-example-mu-two.vercel.app/
Repo: https://github.com/rafaelmagalhaes/next-auth-example

Top comments (0)