Next.js Authentication Basics
Authentication is a critical aspect of web applications that controls access to protected resources and ensures users are who they claim to be. In this guide, we'll explore the fundamentals of implementing authentication in Next.js applications.
What is Authentication?
Authentication is the process of verifying a user's identity. When a user logs in to your application, they're essentially claiming an identity by providing credentials (like a username and password). Authentication verifies these credentials and establishes a session or token that represents the authenticated user.
Authentication vs. Authorization
Before we dive deeper, let's clarify two terms that are often confused:
- Authentication: Verifies who the user is (identity verification)
- Authorization: Determines what resources a user can access (permission control)
This guide focuses primarily on authentication, but we'll touch on authorization concepts as they relate to protecting routes in Next.js.
Authentication Strategies in Next.js
Next.js supports various authentication strategies, each with its own advantages:
- Session-based Authentication: Uses cookies to maintain user sessions
- JWT (JSON Web Token) Authentication: Stateless authentication using encoded tokens
- OAuth/Social Login: Authentication through third-party providers like Google, GitHub, etc.
- Magic Links: Passwordless authentication via email links
- Multi-factor Authentication (MFA): Additional security layers beyond passwords
Setting Up Authentication in Next.js
Let's walk through a basic example of implementing authentication in a Next.js application.
Step 1: Setting Up the Project
First, create a new Next.js application if you don't already have one:
npx create-next-app my-authenticated-app
cd my-authenticated-app
Step 2: Install Authentication Library
For this example, we'll use NextAuth.js, a complete authentication solution for Next.js applications:
npm install next-auth
Step 3: Configure NextAuth.js
Create a new API route for authentication by creating a file at pages/api/auth/[...nextauth].js
:
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export default NextAuth({
providers: [
CredentialsProvider({
// The name to display on the sign in form (e.g. "Sign in with...")
name: "Credentials",
credentials: {
username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
// This is where you would retrieve user data from a database
// For demo purposes, we'll use a hardcoded user
if (
credentials.username === "user" &&
credentials.password === "password"
) {
return {
id: 1,
name: "John Doe",
email: "[email protected]",
};
}
// Return null if user data could not be retrieved
return null;
}
})
],
session: {
strategy: "jwt",
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
async session({ session, token }) {
session.user.id = token.id;
return session;
},
},
pages: {
signIn: '/auth/signin',
}
});
Step 4: Create Sign-In Page
Create a sign-in page at pages/auth/signin.js
:
import { signIn } from "next-auth/react";
import { useState } from "react";
import { useRouter } from "next/router";
export default function SignIn() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
setError("");
const result = await signIn("credentials", {
redirect: false,
username,
password,
});
if (result.error) {
setError("Invalid credentials");
} else {
router.push("/dashboard");
}
};
return (
<div className="login-container">
<h1>Sign In</h1>
{error && <p className="error">{error}</p>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="username">Username</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit">Sign In</button>
</form>
</div>
);
}
Step 5: Configure Provider in _app.js
Wrap your application with the SessionProvider in pages/_app.js
:
import { SessionProvider } from "next-auth/react";
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return (
<SessionProvider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
);
}
export default MyApp;
Step 6: Create Protected Routes
Now let's create a protected dashboard page at pages/dashboard.js
:
import { useSession, signOut } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
export default function Dashboard() {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === "unauthenticated") {
router.push("/auth/signin");
}
}, [status, router]);
if (status === "loading") {
return <div>Loading...</div>;
}
if (!session) {
return null;
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {session.user.name}!</p>
<button onClick={() => signOut({ callbackUrl: '/' })}>
Sign Out
</button>
</div>
);
}
Creating a Custom Auth Hook
For better code organization, let's create a custom hook to handle protected routes:
// hooks/useAuth.js
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
export function useAuth({ required = true } = {}) {
const { data: session, status } = useSession();
const router = useRouter();
const loading = status === "loading";
const authenticated = status === "authenticated";
const unauthenticated = status === "unauthenticated";
useEffect(() => {
if (!loading) {
if (required && unauthenticated) {
router.push("/auth/signin?callbackUrl=" + encodeURIComponent(router.asPath));
} else if (!required && authenticated) {
router.push("/dashboard");
}
}
}, [loading, authenticated, unauthenticated, required, router]);
return {
session,
loading,
authenticated,
unauthenticated,
};
}
Now you can simplify your protected pages:
// pages/dashboard.js
import { useAuth } from "../hooks/useAuth";
import { signOut } from "next-auth/react";
export default function Dashboard() {
const { session, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {session.user.name}!</p>
<button onClick={() => signOut({ callbackUrl: '/' })}>
Sign Out
</button>
</div>
);
}
Server-Side Authentication
Sometimes you need to check authentication on the server side, for example when using getServerSideProps
:
// pages/profile.js
import { getSession } from "next-auth/react";
export default function Profile({ user }) {
return (
<div>
<h1>Profile</h1>
<p>User ID: {user.id}</p>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
);
}
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return {
redirect: {
destination: '/auth/signin',
permanent: false,
},
};
}
return {
props: {
user: session.user,
},
};
}
Authentication with API Routes
To protect API routes, you can also verify the session:
// pages/api/user-data.js
import { getSession } from "next-auth/react";
export default async function handler(req, res) {
const session = await getSession({ req });
if (!session) {
return res.status(401).json({ error: "Unauthorized" });
}
// Access the database or external API securely
// since we know the user is authenticated
return res.status(200).json({
message: "Success",
data: {
userId: session.user.id,
// Other user data...
}
});
}
Best Practices for Authentication
- Never store passwords in plain text: Always hash passwords using bcrypt or similar algorithms.
- Use HTTPS: Always use HTTPS in production to encrypt data in transit.
- Implement rate limiting: Prevent brute force attacks by limiting login attempts.
- Use secure cookies: Set the appropriate security flags on cookies.
- Keep tokens secure: Store JWT tokens securely and implement proper expiration.
- Implement CSRF protection: Protect against cross-site request forgery attacks.
- Consider MFA: Add multi-factor authentication for enhanced security.
Common Authentication Flows
Here are some common authentication flows you might implement:
-
Traditional Username/Password:
- User enters credentials
- Server validates credentials and creates a session
- Session cookie is sent to the client
-
Social Login/OAuth:
- User clicks "Login with Google/GitHub/etc."
- User is redirected to the provider to authenticate
- Provider redirects back with an authorization code
- Server exchanges the code for user information
- Server creates a session for the user
-
Passwordless Authentication:
- User enters email address
- Server sends a one-time link to the email
- User clicks the link and is authenticated
Summary
In this guide, we've covered the essentials of implementing authentication in Next.js applications:
- Understanding authentication concepts
- Setting up NextAuth.js for flexible authentication
- Creating protected routes on both client and server side
- Implementing best practices for secure authentication
- Understanding different authentication flows
Authentication is a complex topic, but Next.js and libraries like NextAuth.js make it manageable by providing standardized approaches to common authentication patterns.
Additional Resources
- NextAuth.js Documentation
- Next.js Authentication Documentation
- OWASP Authentication Best Practices
- JWT.io - Learn about and debug JWTs
Exercises
- Implement social login with Google or GitHub using NextAuth.js
- Create a user registration flow and store user data in a database
- Add password reset functionality to your authentication system
- Implement role-based authorization to restrict access based on user roles
- Set up multi-factor authentication for enhanced security
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)