Implementing Authentication and Authorization using Context API in Next.js: A Comprehensive Guide with Real Code Examples [PART 2]

Implementing Authentication and Authorization using Context API in Next.js: A Comprehensive Guide with Real Code Examples [PART 2]

In the previous part of this article series, we explored the fundamentals of implementing authentication and authorization in a Next.js application using the Context API. We learned how to set up the AuthContext to manage the authentication state throughout the application, handle user login and logout functionality, and restrict access to certain pages based on authentication status.

Recapping briefly, we created a robust authentication system that allowed users to log in, and once authenticated, their information was stored in the AuthContext, enabling seamless access to protected routes. Moreover, we enhanced the security of our application by hashing passwords and implementing JWT for secure token-based authentication.

Now, in this second part, we will delve deeper into advanced authentication features, such as user registration, handling different authorization levels, persisting authentication state even after a page refresh, and securing our API routes. By the end of this article, you'll have a comprehensive understanding of how to build a secure and feature-rich authentication system in your Next.js applications.

Without further ado, let's continue building on the foundations we laid in Part 1 and explore the advanced aspects of authentication and authorization using the Context API in Next.js.

  1. User Registration

In addition to the login functionality, you can implement user registration to allow new users to sign up for your application. Create a new component called Register.js inside the components folder.

// components/Register.js
import { useState } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '../context/authContext';
import { hashPassword } from '../utils/auth';

const Register = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const { setUser } = useAuth();
  const router = useRouter();

  const handleSubmit = async (e) => {
    e.preventDefault();

    // Replace this with your actual user registration logic
    // For example, save user data to a database
    const hashedPassword = await hashPassword(password);
    const user = {
      id: 2,
      email: email,
      password: hashedPassword,
    };

    const token = generateToken({ id: user.id, email: user.email });
    setUser({ ...user, token });
    router.push('/');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        placeholder="Email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        placeholder="Password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Register</button>
    </form>
  );
};

export default Register;

You can then use the Register component in a new registration page (pages/register.js) and include a link to it in your login page.

  1. Handling Authorization Levels

In many applications, different user roles require different levels of authorization. For instance, an admin might have access to certain administrative features that regular users cannot access. You can handle these authorization levels by extending the AuthProvider and adding an isAdmin property to the user object.

Modify the authContext.js file as follows:

// context/authContext.js
// ... (previous code)

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);

  const isAdmin = () => {
    return user && user.role === 'admin';
  };

  return (
    <AuthContext.Provider value={{ user, setUser, isAdmin }}>
      {children}
    </AuthContext.Provider>
  );
};

Now, you can use the isAdmin function within your components to conditionally render certain elements or restrict access to certain pages.

  1. Persisting Authentication State

To maintain the user's authentication state even after a page refresh, we can utilize browser cookies or local storage. In this example, we'll use cookies.

Install the js-cookie package:

npm install js-cookie

Modify the AuthProvider to persist the authentication state:

// context/authContext.js
import Cookies from 'js-cookie';

// ... (previous code)

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // On initial load, retrieve the user from the cookies (if available)
    const userData = Cookies.get('user');
    if (userData) {
      setUser(JSON.parse(userData));
    }
  }, []);

  useEffect(() => {
    // Whenever the user changes, store it in the cookies
    Cookies.set('user', JSON.stringify(user));
  }, [user]);

  // ... (rest of the code)
};
  1. Protecting API Routes

Next.js provides a powerful feature called "API routes," which allow you to create serverless backend endpoints. To protect these endpoints, you can use our withAuth HOC.

For example, let's create an API route to fetch sensitive user data (pages/api/user.js):

// pages/api/user.js
import { withAuth } from '../../utils';

const userHandler = (req, res) => {
  // Replace this with your actual logic to fetch user data from a database
  const user = {
    id: 1,
    email: 'user@example.com',
    role: 'user',
  };

  res.status(200).json(user);
};

export default withAuth(userHandler);

With the withAuth HOC, this API route will only be accessible to authenticated users, preventing unauthorized access to sensitive data.

Conclusion

In this comprehensive guide, we've explored how to implement authentication and authorization using the Context API in Next.js. By creating the AuthContext, handling user login and logout, restricting access to certain pages, and implementing advanced features like user registration and handling authorization levels, we've built a robust and secure authentication system.

However, security is an ongoing process, and it's crucial to stay informed about the latest best practices and vulnerabilities in web application security. Additionally, consider implementing other security measures like rate limiting, CSRF protection, and implementing SSL certificates for secure communication.

Remember, each application is unique, and the security requirements may differ based on the sensitivity of the data being handled. Always conduct thorough security assessments and consider seeking external security audits to ensure the utmost protection for your users and your application.

With the knowledge gained from this article, you're now well-equipped to build secure and performant web applications using Next.js and the Context API. Happy coding!