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
}))
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 />
</>
)
}
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>
)
}
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))
}
}
Pages
The index
page
import Head from 'next/head'
export default function Home() {
return (
<div>
<h1>Homepage</h1>
</div>
)
}
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
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
Preview: https://next-auth-example-mu-two.vercel.app/
Repo: https://github.com/rafaelmagalhaes/next-auth-example
Top comments (0)