NextReady

Authentication

NextReady provides a robust authentication system built on Next-Auth, supporting multiple authentication methods including credentials, Google OAuth, and more.

Overview

NextReady uses Next-Auth (NextAuth.js) for authentication, providing a secure, flexible, and easy-to-use authentication system. Next-Auth supports multiple authentication providers and handles sessions, JWT tokens, and database integration.

Key Features

  • Multiple authentication providers (Credentials, Google, etc.)
  • MongoDB integration for user data storage
  • JWT-based session management
  • Role-based access control
  • Protected routes and API endpoints
  • Custom sign-in and sign-up pages

Configuration

NextReady's authentication system is configured in the src/lib/auth-config.ts file. This file sets up NextAuth.js with the necessary providers, callbacks, and database adapter.

Basic Configuration

// src/lib/auth-config.ts
import { NextAuthOptions } from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import CredentialsProvider from "next-auth/providers/credentials"
import { MongoDBAdapter } from "@auth/mongodb-adapter"
import clientPromise from "./mongodb-client"
import dbConnect from "./mongodb"
import User from "@/models/User"
import bcrypt from "bcryptjs"

export const authOptions: NextAuthOptions = {
  adapter: MongoDBAdapter(clientPromise),
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    CredentialsProvider({
      name: "credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" }
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          return null
        }
        
        await dbConnect()
        
        const user = await User.findOne({ email: credentials.email })
        
        if (!user || !user.password) {
          return null
        }
        
        const isPasswordValid = await bcrypt.compare(
          credentials.password,
          user.password
        )
        
        if (!isPasswordValid) {
          return null
        }
        
        return {
          id: user._id.toString(),
          name: user.name,
          email: user.email,
          image: user.image,
          role: user.role
        }
      }
    })
  ],
  session: {
    strategy: "jwt",
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role
        token.id = user.id
      }
      return token
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.role = token.role
        session.user.id = token.id
      }
      return session
    }
  },
  pages: {
    signIn: "/auth/signin",
    error: "/auth/error",
  },
  secret: process.env.NEXTAUTH_SECRET,
}

Environment Variables

To configure authentication, you need to set the following environment variables in your .env.local file:

# NextAuth Configuration
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key-here

# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret

Dependency Conflict Note

NextReady has a known dependency conflict between next-auth, @auth/core and @auth/mongodb-adapter. To resolve this, make sure to add a .npmrc file with legacy-peer-deps=true to your project.

Authentication Providers

NextReady supports multiple authentication providers through Next-Auth. By default, it includes:

Credentials Provider

The Credentials provider allows users to log in with an email and password. This is configured in the auth-config.ts file:

CredentialsProvider({
  name: "credentials",
  credentials: {
    email: { label: "Email", type: "email" },
    password: { label: "Password", type: "password" }
  },
  async authorize(credentials) {
    // Authentication logic
  }
})

Google Provider

The Google provider allows users to log in with their Google account. To set up Google authentication:

  1. Create a project in the Google Cloud Console
  2. Set up OAuth consent screen
  3. Create OAuth 2.0 credentials
  4. Add the client ID and client secret to your environment variables
GoogleProvider({
  clientId: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
})

Adding Additional Providers

You can easily add more authentication providers by installing the necessary packages and adding them to the providers array in auth-config.ts:

// Example: Adding GitHub provider
import GitHubProvider from "next-auth/providers/github"

// Add to providers array
GitHubProvider({
  clientId: process.env.GITHUB_ID!,
  clientSecret: process.env.GITHUB_SECRET!,
})

Session Management

NextReady uses JWT-based session management. Sessions are stored in cookies and can be accessed on both the client and server side.

Client-Side Session Access

You can access the session on the client side using the useSession hook:

// Client component
'use client'

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

export default function ProfileButton() {
  const { data: session, status } = useSession()
  
  if (status === "loading") {
    return <div>Loading...</div>
  }
  
  if (status === "unauthenticated") {
    return <button>Sign In</button>
  }
  
  return (
    <div>
      <p>Welcome, {session?.user?.name}</p>
      <button>View Profile</button>
    </div>
  )
}

Server-Side Session Access

You can access the session on the server side using the getServerSession function:

// Server component or API route
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth-config"

export async function getData() {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    // Handle unauthenticated user
    return { error: "Not authenticated" }
  }
  
  // Access user information
  const userId = session.user.id
  
  // Fetch user-specific data
  // ...
  
  return { userData: "..." }
}

Protected Routes

NextReady includes middleware to protect routes that require authentication.

Client-Side Protection

You can protect client-side routes using the useSession hook and redirecting unauthenticated users:

// Client component
'use client'

import { useSession } from "next-auth/react"
import { useRouter } from "next/navigation"
import { useEffect } from "react"

export default function ProtectedPage() {
  const { data: session, status } = useSession()
  const router = useRouter()
  
  useEffect(() => {
    if (status === "unauthenticated") {
      router.push("/auth/signin?callbackUrl=/protected-page")
    }
  }, [status, router])
  
  if (status === "loading") {
    return <div>Loading...</div>
  }
  
  if (!session) {
    return null
  }
  
  return (
    <div>
      <h1>Protected Content</h1>
      <p>This page is only visible to authenticated users.</p>
    </div>
  )
}

Server-Side Protection

You can protect server-side routes by checking the session and redirecting if necessary:

// Server component
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth-config"
import { redirect } from "next/navigation"

export default async function ProtectedServerPage() {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    redirect("/auth/signin?callbackUrl=/protected-page")
  }
  
  return (
    <div>
      <h1>Protected Server Content</h1>
      <p>This page is only visible to authenticated users.</p>
    </div>
  )
}

Role-Based Access Control

NextReady supports role-based access control. You can check the user's role to determine if they have access to certain features:

// Check if user is an admin
if (session?.user?.role === "admin") {
  // Show admin features
} else {
  // Show regular user features or redirect
  redirect("/dashboard")
}

User Registration

NextReady includes a custom registration system that works alongside Next-Auth.

Registration API

The registration API is located at /api/auth/signup and handles creating new user accounts:

// src/app/api/auth/signup/route.ts
import { NextResponse } from "next/server"
import bcrypt from "bcryptjs"
import dbConnect from "@/lib/mongodb"
import User from "@/models/User"

export async function POST(request: Request) {
  try {
    const { name, email, password } = await request.json()
    
    // Validate input
    if (!name || !email || !password) {
      return NextResponse.json(
        { error: "Missing required fields" },
        { status: 400 }
      )
    }
    
    await dbConnect()
    
    // Check if user already exists
    const existingUser = await User.findOne({ email })
    if (existingUser) {
      return NextResponse.json(
        { error: "User already exists" },
        { status: 409 }
      )
    }
    
    // Hash password
    const hashedPassword = await bcrypt.hash(password, 10)
    
    // Create new user
    const newUser = await User.create({
      name,
      email,
      password: hashedPassword,
      role: "user",
    })
    
    // Remove password from response
    const user = {
      id: newUser._id.toString(),
      name: newUser.name,
      email: newUser.email,
      role: newUser.role,
    }
    
    return NextResponse.json(user, { status: 201 })
  } catch (error) {
    console.error("Registration error:", error)
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    )
  }
}

Registration Form

NextReady includes a registration form component that submits to the registration API:

// Client component
'use client'

import { useState } from "react"
import { signIn } from "next-auth/react"
import { useRouter } from "next/navigation"

export default function SignUpForm() {
  const [name, setName] = useState("")
  const [email, setEmail] = useState("")
  const [password, setPassword] = useState("")
  const [error, setError] = useState("")
  const [loading, setLoading] = useState(false)
  const router = useRouter()
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    setError("")
    
    try {
      // Register user
      const response = await fetch("/api/auth/signup", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, email, password }),
      })
      
      if (!response.ok) {
        const data = await response.json()
        throw new Error(data.error || "Registration failed")
      }
      
      // Sign in after successful registration
      const result = await signIn("credentials", {
        email,
        password,
        redirect: false,
      })
      
      if (result?.error) {
        throw new Error(result.error || "Sign in failed")
      }
      
      // Redirect to dashboard
      router.push("/dashboard")
    } catch (error: any) {
      setError(error.message)
    } finally {
      setLoading(false)
    }
  }
  
  return (
    <form onSubmit={handleSubmit}>
      {error && (
        <div className="mb-4 p-3 bg-red-50 text-red-500 rounded-lg">
          {error}
        </div>
      )}
      
      <div className="mb-4">
        <label htmlFor="name" className="block mb-2 text-sm font-medium">
          Name
        </label>
        <input
          id="name"
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          className="w-full px-3 py-2 border rounded-lg"
          required
        />
      </div>
      
      <div className="mb-4">
        <label htmlFor="email" className="block mb-2 text-sm font-medium">
          Email
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          className="w-full px-3 py-2 border rounded-lg"
          required
        />
      </div>
      
      <div className="mb-6">
        <label htmlFor="password" className="block mb-2 text-sm font-medium">
          Password
        </label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          className="w-full px-3 py-2 border rounded-lg"
          required
          minLength={8}
        />
      </div>
      
      <button
        type="submit"
        disabled={loading}
        className="w-full py-2 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
      >
        {loading ? "Creating Account..." : "Sign Up"}
      </button>
    </form>
  )
}

Admin Authentication

NextReady includes an admin authentication system that restricts access to administrative features.

Admin Middleware

You can create middleware to protect admin routes:

// src/middleware.ts
import { NextResponse } from "next/server"
import { getToken } from "next-auth/jwt"
import type { NextRequest } from "next/server"

export async function middleware(request: NextRequest) {
  // Check for admin routes
  if (request.nextUrl.pathname.startsWith("/admin")) {
    const token = await getToken({
      req: request,
      secret: process.env.NEXTAUTH_SECRET,
    })
    
    // Check if user is authenticated and has admin role
    if (!token || token.role !== "admin") {
      return NextResponse.redirect(new URL("/auth/signin", request.url))
    }
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: ["/admin/:path*"],
}

Admin API Protection

You can protect admin API routes by checking the user's role:

// src/app/api/admin/route.ts
import { NextResponse } from "next/server"
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth-config"

export async function GET(request: Request) {
  const session = await getServerSession(authOptions)
  
  // Check if user is authenticated and has admin role
  if (!session || session.user.role !== "admin") {
    return NextResponse.json(
      { error: "Unauthorized" },
      { status: 401 }
    )
  }
  
  // Admin-only logic here
  
  return NextResponse.json({ data: "Admin data" })
}

Custom Authentication Logic

You can extend NextReady's authentication system with custom logic to meet your specific requirements.

Custom Callbacks

You can customize Next-Auth's behavior by modifying the callbacks in auth-config.ts:

callbacks: {
  async signIn({ user, account, profile }) {
    // Custom sign-in logic
    // Return true to allow sign in, false to deny
    return true
  },
  async redirect({ url, baseUrl }) {
    // Custom redirect logic
    return url.startsWith(baseUrl) ? url : baseUrl
  },
  async jwt({ token, user, account }) {
    // Add custom properties to JWT token
    if (user) {
      token.customProperty = user.customProperty
    }
    return token
  },
  async session({ session, token }) {
    // Add custom properties to session
    if (session.user) {
      session.user.customProperty = token.customProperty
    }
    return session
  }
}

Custom Pages

You can customize the authentication pages by specifying them in the pages option:

pages: {
  signIn: "/auth/signin",
  signOut: "/auth/signout",
  error: "/auth/error",
  verifyRequest: "/auth/verify-request",
  newUser: "/auth/new-user"
}

Custom Events

You can listen for authentication events using the next-auth/react event listeners:

// Client component
'use client'

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

export default function AuthEvents() {
  const { data: session } = useSession()
  
  useEffect(() => {
    const handleSignIn = (message: any) => {
      console.log("User signed in", message)
      // Custom logic after sign in
    }
    
    const handleSignOut = (message: any) => {
      console.log("User signed out", message)
      // Custom logic after sign out
    }
    
    // Add event listeners
    window.addEventListener("signIn", handleSignIn)
    window.addEventListener("signOut", handleSignOut)
    
    // Clean up
    return () => {
      window.removeEventListener("signIn", handleSignIn)
      window.removeEventListener("signOut", handleSignOut)
    }
  }, [])
  
  return (
    <div>
      {session ? (
        <button onClick={() => signOut()}>Sign Out</button>
      ) : (
        <button onClick={() => signIn()}>Sign In</button>
      )}
    </div>
  )
}

Next Steps

Now that you understand how authentication works in NextReady, you might want to explore: