DEV Community

Nadim Chowdhury
Nadim Chowdhury

Posted on • Edited on

How to create an Authentication & Authorization feature in Next JS 14 with another backend?

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

  1. Initialize a New Project:
   npx create-next-app@latest my-nextjs-app
   cd my-nextjs-app
Enter fullscreen mode Exit fullscreen mode
  1. Install Required Packages:
   npm install next-auth axios
Enter fullscreen mode Exit fullscreen mode

Step 2: Configure NextAuth.js

  1. 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',
     },
   });
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the Custom Backend (Express.js)

  1. Set Up an Express.js Server:
   mkdir my-express-backend
   cd my-express-backend
   npm init -y
   npm install express mongoose bcryptjs jsonwebtoken cors
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. 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);
Enter fullscreen mode Exit fullscreen mode
  1. 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 } });
   };
Enter fullscreen mode Exit fullscreen mode
  1. 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;
Enter fullscreen mode Exit fullscreen mode
  1. 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}`);
   });
Enter fullscreen mode Exit fullscreen mode

Step 4: Set Up Client-Side Authentication in Next.js

  1. 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;
Enter fullscreen mode Exit fullscreen mode
  1. 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;
Enter fullscreen mode Exit fullscreen mode

Usage:

   import withAuth from '../path/to/withAuth';

   const ProtectedPage = () => {
     return (
       <div>
         <h1>Protected Content</h1>
       </div>
     );
   };

   export default withAuth(ProtectedPage);
Enter fullscreen mode Exit fullscreen mode

Step 5: Start the Applications

  1. Run the Next.js Application:
   npm run dev
Enter fullscreen mode Exit fullscreen mode
  1. Run the Express.js Backend:
   node src/index.js
Enter fullscreen mode Exit fullscreen mode

Step 6: Test the Application

  1. 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.
  2. Login a User:

    • Navigate to /login in your Next.js application and log in with the registered user.
  3. 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.

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 } });
};
Enter fullscreen mode Exit fullscreen mode

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 });
  });
});
Enter fullscreen mode Exit fullscreen mode

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;
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • Start Next.js Frontend:
  npm run dev
Enter fullscreen mode Exit fullscreen mode

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)