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)