How to Authenticate Users in Next.js With NextAuth – App Router VS Pages Router

Authentication is a critical feature in most web applications today. It allows you to protect sensitive information, personalize user experiences, and secure access to your app‘s functionality. However, implementing authentication from scratch can be complex and time-consuming.

Fortunately, modern web frameworks like Next.js make it easier to add authentication to your apps. Next.js is a popular React framework that provides a powerful set of tools and conventions for building server-rendered, statically generated, and API-driven applications.

NextAuth is a flexible authentication library that integrates seamlessly with Next.js. It supports multiple authentication providers, including Google, GitHub, Facebook, Twitter, Email, and more. NextAuth also handles session management, token generation, and user profile storage out of the box.

In this article, we‘ll explore how to authenticate users in Next.js applications using NextAuth. We‘ll cover the two main approaches: the traditional Pages Router and the new App Router introduced in Next.js 13. By the end, you‘ll have a solid understanding of how to secure your Next.js apps with robust authentication.

Prerequisites

Before we dive into the implementation details, make sure you have a basic understanding of the following concepts:

  • Next.js fundamentals (pages, routing, data fetching, etc.)
  • React hooks (useState, useEffect, useContext)
  • Authentication principles (sessions, tokens, OAuth)

You‘ll also need to have Node.js and npm (or Yarn) installed on your machine. We‘ll be using the latest version of Next.js (13.x) in this tutorial.

Setting Up the Project

Let‘s start by creating a new Next.js project. Open your terminal and run the following command:

npx create-next-app@latest my-nextauth-app

This will prompt you to choose some options for your project. For this tutorial, we‘ll use the following configuration:

  • TypeScript: No
  • ESLint: Yes
  • Tailwind CSS: No
  • src/ directory: No
  • App Router: Yes
  • Import alias: @/*

Once the project is created, navigate to the project directory:

cd my-nextauth-app

Next, install the required dependencies for NextAuth:

npm install next-auth

Now let‘s compare the folder structure for a Pages Router project vs an App Router project:

Pages Router

my-nextauth-app/
  ├── pages/
  │   ├── api/
  │   │   └── auth/
  │   │       └── [...nextauth].ts
  │   ├── _app.tsx
  │   ├── index.tsx
  │   └── protected.tsx
  ├── public/
  ├── styles/
  └── package.json

App Router

my-nextauth-app/
  ├── app/
  │   ├── api/
  │   │   └── auth/
  │   │       └── [...nextauth]/
  │   │           └── route.ts
  │   ├── page.tsx
  │   ├── layout.tsx  
  │   └── protected/
  │       └── page.tsx
  ├── public/
  ├── styles/
  └── package.json

As you can see, the main difference lies in the top-level directory. The Pages Router uses the pages/ directory to define routes, while the App Router uses the app/ directory with a nested folder structure.

Adding NextAuth

Now that we have our project set up, let‘s configure NextAuth. Create a new file called .env in the root directory and add the following variables:

NEXTAUTH_SECRET=your_secret_here
NEXTAUTH_URL=http://localhost:3000

GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret

GITHUB_CLIENT_ID=your_github_client_id  
GITHUB_CLIENT_SECRET=your_github_client_secret

Replace the placeholders with your actual client IDs and secrets from Google and GitHub. If you don‘t have them yet, you‘ll need to create OAuth apps on their respective developer platforms.

Next, let‘s create the API routes for authentication. For the Pages Router, create a new file called […nextauth].ts inside the pages/api/auth directory:

// pages/api/auth/[...nextauth].ts

import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import GitHubProvider from "next-auth/providers/github"

export default NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET,
    }),
  ],
})

This sets up NextAuth with Google and GitHub as authentication providers. The […nextauth] syntax allows NextAuth to handle various authentication routes like /api/auth/signin, /api/auth/callback, etc.

For the App Router, create a new file called route.ts inside the app/api/auth/[…nextauth] directory:

// app/api/auth/[...nextauth]/route.ts

import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import GitHubProvider from "next-auth/providers/github"

const handler = NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET,
    }),
  ],
})

export { handler as GET, handler as POST }

The configuration is similar, but we export the handler as both GET and POST methods to match the new App Router conventions.

Pages Router Implementation

With the Pages Router, we can access authentication state using the useSession hook provided by NextAuth. First, wrap your pages/_app.tsx component with the SessionProvider:

// pages/_app.tsx

import { SessionProvider } from "next-auth/react"
import type { AppProps } from "next/app"

export default function App({ 
  Component, 
  pageProps: { session, ...pageProps },
}: AppProps) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  )
}

Then, you can use the useSession hook in any page or component to access the current user session:

// pages/index.tsx

import { useSession, signIn, signOut } from "next-auth/react"

export default function Home() {
  const { data: session } = useSession()

  if (session) {
    return (
      <>
        <p>Signed in as {session.user.email}</p>
        <button onClick={() => signOut()}>Sign Out</button>
      </>
    )
  }

  return (
    <>
      <p>Not signed in</p>  
      <button onClick={() => signIn()}>Sign In</button>
    </>
  )
}

To protect certain pages, you can create a custom higher-order component (HOC) or use NextAuth‘s built-in middleware:

// pages/protected.tsx

import { useSession } from "next-auth/react"

export default function ProtectedPage() {
  const { status } = useSession({
    required: true,
    onUnauthenticated() {
      // The user is not authenticated, redirect to sign-in
    },
  })

  if (status === "loading") {
    return <p>Loading...</p>
  }

  return <p>This is a protected page. Only authenticated users can see it.</p>
}

App Router Implementation

With the App Router, authentication works a bit differently. Instead of using hooks, you can access the session data directly in server components.

First, create a new file called session.ts to get the session on the server:

// app/session.ts

import { getServerSession } from "next-auth"

export default async function getSession() {
  return await getServerSession()
}

Then, you can use this function in any server component to protect routes or access user data:

// app/protected/page.tsx

import { redirect } from "next/navigation"
import getSession from "../session"

export default async function ProtectedPage() {
  const session = await getSession()

  if (!session) {
    redirect("/api/auth/signin")
  }

  return <p>This is a protected page. Only authenticated users can see it.</p>
}  

For client components, you can fetch the session data from a parent server component and pass it down as props:

// app/page.tsx

import { Inter } from "next/font/google"
import styles from "./page.module.css"
import getSession from "./session"
import AuthButton from "./AuthButton"

const inter = Inter({ subsets: ["latin"] })

export default async function Home() {
  const session = await getSession()

  return (
    <main className={styles.main}>
      <h1 className={inter.className}>Welcome to Next.js with NextAuth!</h1>
      <AuthButton session={session} />
    </main>
  )
}
// app/AuthButton.tsx

"use client"

import { signIn, signOut } from "next-auth/react"

export default function AuthButton({ session }) {
  if (session) {
    return (
      <>
        <p>Signed in as {session.user.email}</p>
        <button onClick={() => signOut()}>Sign Out</button>
      </>  
    )
  }

  return (
    <>
      <p>Not signed in</p>
      <button onClick={() => signIn()}>Sign In</button>
    </>
  )
}

Note the "use client" directive at the top of AuthButton.tsx. This tells Next.js that it‘s a client component, allowing us to use browser-only features like signIn and signOut.

Testing Authentication Flow

To test the authentication flow, run your Next.js app in development mode:

npm run dev

Open your browser and navigate to http://localhost:3000. You should see a "Sign In" button. Clicking it will take you to the NextAuth sign-in page, where you can choose to authenticate with Google or GitHub.

After successfully signing in, you‘ll be redirected back to the home page, which should now display your email address and a "Sign Out" button. Clicking the "Sign Out" button will log you out and return you to the unauthenticated state.

Try accessing the /protected route. If you‘re not signed in, you should be redirected to the sign-in page. Once authenticated, you should be able to view the protected content.

Best Practices and Tips

Here are some best practices and tips to keep in mind when implementing authentication in Next.js with NextAuth:

  • Always use HTTPS in production to secure your authentication flow and protect user data.
  • Store sensitive information like client secrets and database credentials in environment variables, not in your codebase.
  • Use secure, HTTP-only cookies to store session tokens on the client-side.
  • Implement CSRF protection to prevent cross-site request forgery attacks.
  • Handle expired sessions gracefully by redirecting users to the sign-in page or showing an "Access Denied" message.
  • Customize the user interface and email templates to match your app‘s branding and style.

Conclusion

In this article, we explored two approaches to implementing authentication in Next.js applications using NextAuth: the Pages Router and the App Router.

The Pages Router approach relies on the useSession hook to access authentication state in pages and components, while the App Router approach uses server components to fetch session data and pass it down to client components as props.

Both approaches have their pros and cons. The Pages Router is simpler and more familiar to developers used to traditional React apps, but it can lead to prop drilling and complex component hierarchies. The App Router offers a more unified and efficient way to handle authentication, but it requires a deeper understanding of server and client components.

Ultimately, the choice between the two approaches depends on your project‘s specific needs and constraints. If you‘re building a small to medium-sized app with limited authentication requirements, the Pages Router might suffice. But if you‘re working on a large-scale, feature-rich application with complex authorization rules, the App Router might be a better fit.

As Next.js continues to evolve and mature, it‘s likely that authentication patterns and best practices will also change. Keep an eye out for updates to the NextAuth library and the Next.js documentation to stay up-to-date with the latest trends and techniques in web authentication.

By following the steps outlined in this article and adhering to best practices, you can create secure, scalable, and user-friendly Next.js applications that delight your users and protect their data. Happy coding!

Similar Posts