To create an authentication and authorization feature in Next.js 14 with another backend, you typically need to use a third-party authentication provider or a custom backend API. Here, I'll demonstrate how to do this using Next.js with NextAuth.js as the authentication library and a custom backend (e.g., an Express.js server) for handling authentication logic.
Step 1: Set Up the Next.js Project
- Initialize a New Project:
npx create-next-app@latest my-nextjs-app
cd my-nextjs-app
- Install Required Packages:
npm install next-auth axios
Step 2: Configure NextAuth.js
-
Create the Auth API Route:
Create
src/pages/api/auth/[...nextauth].js
:
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import axios from 'axios';
export default NextAuth({
providers: [
Providers.Credentials({
async authorize(credentials) {
try {
const res = await axios.post('http://localhost:4000/api/auth/login', {
email: credentials.email,
password: credentials.password,
});
if (res.data && res.data.token) {
return { token: res.data.token, user: res.data.user };
}
return null;
} catch (error) {
throw new Error('Invalid email or password');
}
},
}),
],
callbacks: {
async jwt(token, user) {
if (user) {
token.accessToken = user.token;
token.user = user.user;
}
return token;
},
async session(session, token) {
session.accessToken = token.accessToken;
session.user = token.user;
return session;
},
},
pages: {
signIn: '/login',
},
});
Step 3: Create the Custom Backend (Express.js)
- Set Up an Express.js Server:
mkdir my-express-backend
cd my-express-backend
npm init -y
npm install express mongoose bcryptjs jsonwebtoken cors
-
Configure Environment Variables:
Create a
.env
file in the root of your backend project:
PORT=4000
MONGODB_URI=mongodb://localhost:27017/auth-db
JWT_SECRET=your_jwt_secret
-
Create User Model:
Create
src/models/User.js
:
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const UserSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
});
UserSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
UserSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
module.exports = mongoose.model('User', UserSchema);
-
Create Authentication Controller:
Create
src/controllers/authController.js
:
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const bcrypt = require('bcryptjs');
exports.registerUser = async (req, res) => {
const { name, email, password } = req.body;
const userExists = await User.findOne({ email });
if (userExists) return res.status(400).json({ message: 'User already exists' });
const user = await User.create({ name, email, password });
if (user) {
res.status(201).json({ message: 'User registered successfully' });
} else {
res.status(400).json({ message: 'Invalid user data' });
}
};
exports.loginUser = async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await user.matchPassword(password))) {
return res.status(401).json({ message: 'Invalid email or password' });
}
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token, user: { id: user._id, name: user.name, email: user.email } });
};
-
Create Routes:
Create
src/routes/authRoutes.js
:
const express = require('express');
const { registerUser, loginUser } = require('../controllers/authController');
const router = express.Router();
router.post('/register', registerUser);
router.post('/login', loginUser);
module.exports = router;
-
Set Up Express.js Server:
Create
src/index.js
:
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const authRoutes = require('./routes/authRoutes');
require('dotenv').config();
const app = express();
app.use(cors());
app.use(express.json());
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
app.use('/api/auth', authRoutes);
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 4: Set Up Client-Side Authentication in Next.js
-
Create Login Page:
Create
src/pages/login.js
:
import { useState } from 'react';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/router';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
const result = await signIn('credentials', {
redirect: false,
email,
password,
});
if (result.ok) {
router.push('/');
} else {
setError(result.error);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Email</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<div>
<label>Password</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
</div>
{error && <p>{error}</p>}
<button type="submit">Login</button>
</form>
);
};
export default Login;
- Protect Pages (Client-Side): Create a higher-order component (HOC) to protect pages:
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
const withAuth = (WrappedComponent) => {
return (props) => {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === 'unauthenticated') {
router.replace('/login');
}
}, [status, router]);
if (status === 'authenticated') {
return <WrappedComponent {...props} />;
}
return null;
};
};
export default withAuth;
Usage:
import withAuth from '../path/to/withAuth';
const ProtectedPage = () => {
return (
<div>
<h1>Protected Content</h1>
</div>
);
};
export default withAuth(ProtectedPage);
Step 5: Start the Applications
- Run the Next.js Application:
npm run dev
- Run the Express.js Backend:
node src/index.js
Step 6: Test the Application
-
Register a User:
- Use a tool like Postman to send a POST request to
http://localhost:4000/api/auth/register
with the user's details.
- Use a tool like Postman to send a POST request to
-
Login a User:
- Navigate to
/login
in your Next.js application and log in with the registered user.
- Navigate to
-
Access Protected Page:
- Navigate to a protected page, e.g.,
/profile
, to ensure it redirects to/login
when not authenticated and displays the content when authenticated.
- Navigate to a protected page, e.g.,
This guide provides a foundational approach to implementing authentication and authorization in a Next.js 14 application with a separate backend. You can further expand and customize it based on your application's requirements.
To implement a refresh token system and automatically attach tokens to Axios requests in your Next.js 14 application with an Express backend, follow these steps:
1. Modify the Backend to Support Refresh Tokens
In your authController.js, update the login function to generate both access and refresh tokens:
Update loginUser
Function
const jwt = require('jsonwebtoken');
const User = require('../models/User');
exports.loginUser = async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await user.matchPassword(password))) {
return res.status(401).json({ message: 'Invalid email or password' });
}
// Generate Access Token (Short Expiry)
const accessToken = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '15m' });
// Generate Refresh Token (Long Expiry)
const refreshToken = jwt.sign({ id: user._id }, process.env.JWT_REFRESH_SECRET, { expiresIn: '7d' });
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production', // Use secure cookies in production
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});
res.json({ accessToken, user: { id: user._id, name: user.name, email: user.email } });
};
Create a Refresh Token Route
In authRoutes.js, add a new route to refresh the access token:
router.post('/refresh-token', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ message: 'Unauthorized' });
}
jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET, (err, decoded) => {
if (err) {
return res.status(403).json({ message: 'Invalid refresh token' });
}
const newAccessToken = jwt.sign({ id: decoded.id }, process.env.JWT_SECRET, { expiresIn: '15m' });
res.json({ accessToken: newAccessToken });
});
});
2. Modify NextAuth to Handle Refresh Tokens
In src/pages/api/auth/[...nextauth].js
, modify the authentication flow:
Update Callbacks
export default NextAuth({
providers: [
CredentialsProvider({
async authorize(credentials) {
try {
const res = await axios.post('http://localhost:4000/api/auth/login', {
email: credentials.email,
password: credentials.password,
});
if (res.data && res.data.accessToken) {
return { accessToken: res.data.accessToken, user: res.data.user };
}
return null;
} catch (error) {
throw new Error('Invalid email or password');
}
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.accessToken = user.accessToken;
}
return token;
},
async session({ session, token }) {
session.accessToken = token.accessToken;
return session;
},
},
});
3. Automatically Attach Tokens to Axios Requests
To ensure every request is authenticated, create an Axios instance:
Create api.js
in src/utils/api.js
import axios from 'axios';
import { signOut, getSession } from 'next-auth/react';
const api = axios.create({
baseURL: 'http://localhost:4000/api',
withCredentials: true, // Ensures cookies (refresh token) are sent with requests
});
// Add a request interceptor
api.interceptors.request.use(async (config) => {
const session = await getSession();
if (session?.accessToken) {
config.headers.Authorization = `Bearer ${session.accessToken}`;
}
return config;
}, (error) => Promise.reject(error));
// Add a response interceptor
api.interceptors.response.use((response) => response, async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Try refreshing the token
const { data } = await axios.post('http://localhost:4000/api/auth/refresh-token', {}, { withCredentials: true });
if (data.accessToken) {
// Update session with the new access token
const session = await getSession();
session.accessToken = data.accessToken;
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
return api(originalRequest);
}
} catch (refreshError) {
console.error('Refresh token expired, logging out...');
signOut();
}
}
return Promise.reject(error);
});
export default api;
4. Use api.js
in Protected Requests
Instead of axios
, import and use api.js
for authenticated requests:
import api from '../utils/api';
const fetchUserProfile = async () => {
try {
const response = await api.get('/user/profile');
console.log(response.data);
} catch (error) {
console.error('Error fetching profile', error);
}
};
5. Summary of Improvements
β
Short-Lived Access Token: Prevents prolonged exposure to security risks.
β
Refresh Token System: Users stay logged in without frequent re-authentication.
β
Axios Interceptors: Automatically attaches access tokens and refreshes them when expired.
β
Secure Cookies: Stores the refresh token in HTTP-only cookies, preventing XSS attacks.
6. Running the Application
Run your projects:
- Start Express Backend:
node src/index.js
- Start Next.js Frontend:
npm run dev
Now, your Next.js app should properly authenticate users, refresh tokens when expired, and handle requests securely. π
Support My Work β€οΈ
If you enjoy my content and find it valuable, consider supporting me by buying me a coffee. Your support helps me continue creating and sharing useful resources. Thank you!
Connect with Me π
Letβs stay connected! You can follow me or reach out on these platforms:
πΉ YouTube β Tutorials, insights & tech content
πΉ LinkedIn β Professional updates & networking
πΉ GitHub β My open-source projects & contributions
πΉ Instagram β Behind-the-scenes & personal updates
πΉ X (formerly Twitter) β Quick thoughts & tech discussions
Iβd love to hear from youβwhether itβs feedback, collaboration ideas, or just a friendly hello!
Disclaimer
This content has been generated with the assistance of AI. While I strive for accuracy and quality, please verify critical information independently.
Top comments (0)