Next.js Lesson 18 – Authentication Basics | Dataplexa
LESSON 18

Authentication Basics

Build secure user login and registration for NewsWave using multiple authentication strategies.
Authentication forms the backbone of modern web applications. Without it, you cannot identify users, protect content, or personalize experiences. Think of authentication as checking someone's ID at a club entrance — you need to verify who they are before letting them inside. Next.js provides multiple ways to handle authentication, each suited for different scenarios. You might use simple email/password forms, third-party providers like Google, or enterprise solutions. The NewsWave platform needs user accounts so readers can save articles, comment, and access premium content. Authentication differs from authorization — a concept we'll explore in the next lesson. Authentication answers "who are you?" while authorization answers "what can you do?" Like having a driver's license (authentication) versus having permission to drive a company car (authorization). The complexity varies dramatically based on your needs. A simple blog might only need admin login. An e-commerce site requires user accounts, password reset, and profile management. Enterprise applications often integrate with existing identity systems.

Authentication Strategies

Next.js supports several authentication patterns, each with distinct advantages and use cases. Understanding these patterns helps you choose the right approach for your application's requirements. Session-based authentication works like a hotel key card. When you log in, the server creates a session and gives you a session ID cookie. Every request includes this cookie, and the server looks up your session to verify identity. Sessions live on the server, making them secure but requiring server storage. Token-based authentication uses JSON Web Tokens (JWTs) — like carrying a tamper-proof passport. The server signs a token containing user information, and the client stores it. Each request includes the token, which the server verifies without database lookups. Tokens work well for APIs and distributed systems.
// Session-based: Server stores user data
const session = {
  userId: 123,
  email: 'reader@newswave.com',
  expires: new Date('2024-12-31')
}

// Token-based: Client stores signed user data  
const jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...'
OAuth and social login delegate authentication to trusted providers like Google, GitHub, or Twitter. Instead of storing passwords, you redirect users to the provider, receive an access token, and use it to fetch user information. This reduces security risks and improves user experience. Third-party authentication services like Auth0, Clerk, or Supabase handle the complex parts for you. They provide login forms, password reset, social providers, and security features. You integrate their SDK and focus on your application logic instead of authentication infrastructure.
Sessions
Server storage, secure cookies, traditional web apps
JWTs
Stateless, API-friendly, mobile compatible
OAuth
Social login, no password storage, trusted providers
Third-party
Full-featured, managed security, quick integration

NextAuth.js Overview

NextAuth.js stands as the most popular authentication library for Next.js applications. Built specifically for Next.js, it handles the complexity of modern authentication while providing a clean developer experience. The library supports multiple providers, databases, and authentication flows out of the box. NextAuth.js works by creating API routes that handle authentication logic. When users log in, these routes process credentials, verify identity, and manage sessions or tokens. The library provides React hooks to access user data throughout your application. The architecture centers around providers, adapters, and sessions. Providers handle different login methods — email/password, Google OAuth, GitHub, etc. Adapters connect to databases for storing user accounts and sessions. Sessions maintain user state between requests.
# Install NextAuth.js for NewsWave authentication
npm install next-auth

# Optional: Install database adapter
npm install @next-auth/prisma-adapter
Terminal
$ npm install next-auth
+ next-auth@4.24.5
added 42 packages, and audited 284 packages in 3s
✓ NextAuth.js installed successfully
Configuration happens in a single file that defines providers, database connections, and authentication options. NextAuth.js follows convention over configuration — sensible defaults work for most use cases, but everything remains customizable. The library handles security concerns automatically. It generates secure session tokens, implements CSRF protection, rotates secrets, and follows OAuth specifications. You get enterprise-grade security without becoming a security expert.
What just happened?
NextAuth.js provides authentication infrastructure for Next.js applications. It handles multiple login providers, manages user sessions, and implements security best practices. The library reduces authentication complexity from weeks of development to a few configuration files.

Basic Setup Configuration

Setting up NextAuth.js requires creating configuration files and environment variables. The process involves defining authentication providers, configuring session handling, and setting up API routes. Start by creating the main configuration file. NextAuth.js expects this file to export providers, session options, and callback functions. Each provider requires specific configuration — API keys for OAuth providers, database connections for credentials, etc.
// lib/auth.js - NextAuth configuration for NewsWave
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import CredentialsProvider from 'next-auth/providers/credentials'

export const authOptions = {
  providers: [
    // Google OAuth for social login
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],
}
Environment variables store sensitive configuration like API keys and secrets. NextAuth.js requires a secret for signing tokens and cookies. OAuth providers need client IDs and secrets obtained from their developer portals.
# .env.local - Authentication secrets for NewsWave
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key-here

# Google OAuth credentials
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
Terminal
# Environment variables loaded
✓ NEXTAUTH_URL configured
✓ NEXTAUTH_SECRET set
# Ready for authentication setup
The API route handles authentication requests. NextAuth.js provides a dynamic route handler that processes login, logout, and callback requests. This single route manages all authentication flows.
// app/api/auth/[...nextauth]/route.js - Authentication API routes
import NextAuth from 'next-auth'
import { authOptions } from '@/lib/auth'

// Handle all authentication requests
const handler = NextAuth(authOptions)

// Export for GET and POST requests
export { handler as GET, handler as POST }
Session configuration controls how user data persists between requests. You can choose JWT tokens for stateless authentication or database sessions for traditional server-side storage. Each approach has performance and scalability implications.
What just happened?
NextAuth.js configuration defines authentication providers, stores sensitive keys in environment variables, and creates API routes for handling login requests. The setup follows Next.js conventions — configuration files in lib/, environment variables in .env.local, and API routes in app/api/.

Session Management

Session management determines how your application tracks authenticated users across requests. NextAuth.js provides two session strategies — JWT tokens stored in cookies or database sessions with session IDs. JWT sessions store user information directly in signed cookies. When users log in, NextAuth.js creates a JWT containing user ID, email, and expiration time. This token travels with each request, and the server verifies the signature to confirm authenticity. JWTs work well for serverless applications since they require no database lookups. Database sessions store minimal session IDs in cookies while keeping user data on the server. This approach provides better security and allows immediate session invalidation. However, it requires database queries for each authenticated request and doesn't work well with serverless functions.
// lib/auth.js - Session configuration for NewsWave
export const authOptions = {
  providers: [/* providers here */],
  
  // Use JWT sessions for serverless compatibility
  session: {
    strategy: 'jwt',
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
  
  // Customize JWT token content
  jwt: {
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
}
Session callbacks allow customizing the data available in client-side sessions. The JWT callback runs when tokens are created or accessed, letting you add custom fields. The session callback shapes what data reaches your React components.
// lib/auth.js - Custom session data for NewsWave users
export const authOptions = {
  callbacks: {
    // Add custom data to JWT tokens
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role || 'reader' // Default role for NewsWave users
        token.subscription = user.subscription || 'free'
      }
      return token
    },
    
    // Shape session data for client components
    async session({ session, token }) {
      session.user.role = token.role
      session.user.subscription = token.subscription
      return session
    },
  },
}
React hooks provide access to session data throughout your application. The useSession hook returns current user information and loading states. The signIn and signOut functions handle authentication actions.
// components/UserProfile.js - Access session data in NewsWave
'use client'
import { useSession, signOut } from 'next-auth/react'

export default function UserProfile() {
  const { data: session, status } = useSession()
  
  if (status === 'loading') return <div>Loading...</div>
  if (!session) return <div>Please sign in</div>
  
  return (
    <div>
      <h3>Welcome, {session.user.name}</h3>
      <p>Role: {session.user.role}</p>
      <button onClick={() => signOut()}>Sign Out</button>
    </div>
  )
}
localhost:3000 — NewsWave
What just happened?
Session management controls user state across requests. JWT sessions store user data in signed cookies, while callbacks customize available data. React hooks provide session access in components, handling loading states and authentication actions automatically.

Provider Configuration

Authentication providers define how users can log into your application. NextAuth.js supports dozens of built-in providers — from social logins like Google and GitHub to enterprise systems like Active Directory. Each provider requires specific configuration and credentials. OAuth providers like Google, GitHub, and Twitter redirect users to their authentication servers. Users grant permission, and the provider sends back an access token. NextAuth.js uses this token to fetch user information and create sessions. OAuth eliminates password storage and leverages trusted identity providers. Setting up OAuth requires registering your application with each provider. They provide client IDs and secrets that authenticate your app to their services. The provider configuration includes these credentials plus optional scope and parameter customizations.
// lib/auth.js - Multiple OAuth providers for NewsWave
import GoogleProvider from 'next-auth/providers/google'
import GitHubProvider from 'next-auth/providers/github'
import TwitterProvider from 'next-auth/providers/twitter'

export const authOptions = {
  providers: [
    // Google OAuth - most popular social login
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
    
    // GitHub OAuth - developer-friendly
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET,
    }),
  ],
}
Credentials providers handle traditional email/password authentication. Unlike OAuth, you control the entire authentication flow — form submission, credential verification, and user lookup. This approach works well for custom user systems or when social login isn't appropriate.
// lib/auth.js - Email/password authentication for NewsWave
import CredentialsProvider from 'next-auth/providers/credentials'

export const authOptions = {
  providers: [
    CredentialsProvider({
      name: 'Email and Password',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' }
      },
      
      // Verify credentials against your database
      async authorize(credentials) {
        const user = await verifyCredentials(
          credentials.email, 
          credentials.password
        )
        
        if (user) {
          return {
            id: user.id,
            name: user.name,
            email: user.email,
            role: user.role
          }
        }
        return null // Invalid credentials
      }
    }),
  ],
}
Provider callbacks customize the authentication flow. The signIn callback controls whether to allow login based on user data or external conditions. The redirect callback determines where users go after authentication.
// lib/auth.js - Custom authentication flow for NewsWave
export const authOptions = {
  providers: [/* providers */],
  
  callbacks: {
    // Control who can sign in
    async signIn({ user, account, profile }) {
      // Block users without verified email
      if (account.provider === 'google' && !profile.email_verified) {
        return false
      }
      
      // Only allow specific domains for NewsWave staff
      if (user.email.endsWith('@newswave.com')) {
        user.role = 'editor'
      }
      
      return true // Allow sign in
    },
    
    // Customize redirect after authentication  
    async redirect({ url, baseUrl }) {
      // Redirect to dashboard for authenticated users
      return url.startsWith(baseUrl) ? url : `${baseUrl}/dashboard`
    }
  },
}
Terminal
# Provider configuration loaded
✓ Google OAuth configured
✓ GitHub OAuth configured
✓ Credentials provider ready
# Multiple login options available
What just happened?
Authentication providers define login methods for your application. OAuth providers handle social login through trusted services, while credentials providers manage email/password authentication. Callbacks customize the authentication flow, controlling access and post-login redirects.

Login and Logout Implementation

Implementing login and logout functionality involves creating user interfaces and connecting them to NextAuth.js functions. The library provides signIn and signOut functions that handle the authentication flow automatically. Login buttons trigger authentication flows based on the provider type. OAuth providers redirect to external services, while credentials providers submit forms to your API routes. NextAuth.js manages the complexity — CSRF protection, callback handling, and error management.
// components/LoginButton.js - Social login for NewsWave readers
'use client'
import { signIn, signOut, useSession } from 'next-auth/react'

export default function LoginButton() {
  const { data: session } = useSession()
  
  // Show logout if user is authenticated
  if (session) {
    return (
      <div>
        <span>Welcome, {session.user.name}!</span>
        <button onClick={() => signOut()}>
          Sign Out
        </button>
      </div>
    )
  }
  
  // Show login options for anonymous users
  return (
    <div>
      <button onClick={() => signIn('google')}>
        Sign in with Google
      </button>
      <button onClick={() => signIn('github')}>
        Sign in with GitHub  
      </button>
    </div>
  )
}
localhost:3000 — NewsWave
Custom login forms give you complete control over the user experience. Instead of redirecting to provider pages, you can embed authentication directly into your design. This approach works well for credentials providers or when you need branded login experiences.
// components/CustomLoginForm.js - Custom email/password form  
'use client'
import { signIn } from 'next-auth/react'
import { useState } from 'react'

export default function CustomLoginForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [loading, setLoading] = useState(false)
  
  const handleSubmit = async (e) => {
    e.preventDefault()
    setLoading(true)
    
    // Submit credentials to NextAuth
    const result = await signIn('credentials', {
      email,
      password,
      redirect: false // Handle result manually
    })
    
    if (result?.error) {
      alert('Login failed: ' + result.error)
    }
    setLoading(false)
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="email"
        placeholder="Email address"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        required 
      />
      <input 
        type="password"
        placeholder="Password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        required 
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Signing in...' : 'Sign In'}
      </button>
    </form>
  )
}
Session providers wrap your application to make authentication state available everywhere. Place the provider at the root level so all components can access session data through hooks. This eliminates prop drilling and simplifies authentication checks.
// app/layout.js - Wrap NewsWave with session provider
'use client'
import { SessionProvider } from 'next-auth/react'

export default function RootLayout({ children, session }) {
  return (
    <html lang="en">
      <body>
        {/* Make session available to all components */}
        <SessionProvider session={session}>
          <header>
            <h1>NewsWave</h1>
            <LoginButton />
          </header>
          <main>{children}</main>
        </SessionProvider>
      </body>
    </html>
  )
}
Security Note
Never store passwords in plain text. Always hash passwords using bcrypt or similar libraries before storing in databases. NextAuth.js handles this automatically for credentials providers when you implement proper password verification.

Route Protection

Route protection prevents unauthorized users from accessing specific pages or API endpoints. Next.js provides multiple ways to implement protection — middleware for global checks, higher-order components for page-level protection, and manual checks within components. Middleware runs before every request, making it perfect for route protection. You can check authentication status and redirect unauthorized users before pages load. This approach works at the edge and provides fast, secure access control.
// middleware.js - Protect NewsWave dashboard and admin pages
import { withAuth } from 'next-auth/middleware'

export default withAuth(
  function middleware(req) {
    // Additional logic can go here
    console.log('Protected route accessed:', req.nextUrl.pathname)
  },
  {
    callbacks: {
      // Check if user can access the page
      authorized: ({ token, req }) => {
        const { pathname } = req.nextUrl
        
        // Admin routes require admin role
        if (pathname.startsWith('/admin')) {
          return token?.role === 'admin'
        }
        
        // Dashboard requires any authenticated user
        if (pathname.startsWith('/dashboard')) {
          return !!token
        }
        
        return true // Allow public routes
      },
    },
  }
)

// Specify which routes to protect
export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*']
}
Terminal
# Middleware active for route protection
✓ /dashboard/* routes protected
✓ /admin/* routes protected
# Unauthorized users redirected to login
Server-side protection verifies authentication during page rendering. This approach works well for SSR pages that need user data or want to avoid content flashing. Use getServerSession to check authentication status on the server.
// app/dashboard/page.js - Server-side protection for NewsWave dashboard
import { getServerSession } from 'next-auth/next'
import { authOptions } from '@/lib/auth'
import { redirect } from 'next/navigation'

export default async function Dashboard() {
  // Check authentication on the server
  const session = await getServerSession(authOptions)
  
  // Redirect to login if not authenticated
  if (!session) {
    redirect('/api/auth/signin')
  }
  
  return (
    <div>
      <h1>NewsWave Dashboard</h1>
      <p>Welcome back, {session.user.name}!</p>
      {/* Dashboard content here */}
    </div>
  )
}
Client-side protection provides dynamic route guarding within React components. This method shows loading states and handles conditional rendering based on authentication status. It works well for single-page application flows.
// components/ProtectedPage.js - Client-side route protection
'use client'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'

export default function ProtectedPage({ children }) {
  const { data: session, status } = useSession()
  const router = useRouter()
  
  useEffect(() => {
    // Redirect unauthorized users
    if (status === 'unauthenticated') {
      router.push('/api/auth/signin')
    }
  }, [status, router])
  
  // Show loading while checking authentication
  if (status === 'loading') {
    return <div>Checking authentication...</div>
  }
  
  // Show login prompt for unauthenticated users
  if (!session) {
    return <div>Please sign in to access this page</div>
  }
  
  // Render protected content
  return children
}
What just happened?
Route protection controls access to pages and API endpoints. Middleware provides edge-level protection, server-side checks handle SSR authentication, and client-side guards enable dynamic protection. Multiple approaches ensure comprehensive access control across your application.
Common Authentication Pitfalls
Avoid storing sensitive tokens in localStorage — use secure cookies. Don't implement custom password hashing — use established libraries. Never trust client-side authentication checks alone — always verify on the server. Remember that JWTs cannot be invalidated until they expire.

Quiz

1. NewsWave wants to implement stateless authentication for their API. How do JWT sessions work in NextAuth.js?


2. The NewsWave team needs to protect their /dashboard and /admin routes. Which file should contain the route protection logic that runs before every request?


3. NewsWave wants to restrict Google OAuth login to users with verified email addresses. What NextAuth.js callback should they customize?


Up Next: Authorization

Master role-based access control, permissions, and fine-grained authorization patterns to secure NewsWave features by user type and subscription level.