diff --git a/content/user-authentication-in-node/index.md b/content/user-authentication-in-node/index.md new file mode 100644 index 00000000..65affc77 --- /dev/null +++ b/content/user-authentication-in-node/index.md @@ -0,0 +1,373 @@ +--- +title: How to Authenticate and Authorize Users in a Node.js Express App +date: "2024-12-06" +description: "A step by step guide on how to Authenticate and Authorize Users in a Node.js Express App" +cover: "user-authentication-in-node.png" +category: "programming" +author: "Mostafa Ibrahim" +--- + +# Table of Contents + +- [Introduction](#introduction) +- [Handling Authorization in Node.js](#handling-authorization-in-nodejs) +- [Node.js User Authentication Prerequisites](#nodejs-user-authentication-prerequisites) +- [How to Implement User Authentication in Node.js](#how-to-implement-user-authentication-in-nodejs) + - [Backend Implementation](#backend-implementation) + - [Step 1: User Registration Process](#step-1-user-registration-process) + - [Step 2: Login and Session Management](#step-2-login-and-session-management) + - [Step 3: Protecting Routes with Middleware](#step-3-protecting-routes-with-middleware) + - [Frontend Implementation](#frontend-implementation) +- [Common Pitfalls and Security Best Practices](#common-pitfalls-and-security-best-practices) +- [Simplify Authentication with Supertokens](#simplify-authentication-with-supertokens) +- [Conclusion](#conclusion) + +# **Introduction** +Authentication and authorization are essential for web applications to ensure that only legitimate users gain access and that they can perform actions based on their permissions. **Node.js** and **Express.js** are popular tools for building these systems because they allow developers to create fast, scalable apps efficiently. + +However, implementing authentication can be challenging—**securing passwords, managing sessions**, and ensuring tokens aren’t misused require careful planning. Authorization adds more complexity by defining user roles and permissions, ensuring users only access what they’re allowed to. Good user management is critical for protecting both **user data** and **application security**, while also maintaining a smooth user experience. +# **Handling Authorization in Node.js** + +#### **Difference Between Authentication and Authorization** +- **Authentication:** + This process confirms the **identity of the user**—essentially verifying who they are. It typically involves methods like **username and password checks**, **social login**, or **multi-factor authentication** (MFA). For example, when a user logs into an application, authentication ensures that the individual is who they claim to be. +- **Authorization:** + Authorization determines **what actions or resources the authenticated user can access**. Once a user is authenticated, authorization checks whether they have the necessary permissions to perform specific operations, such as editing content or accessing admin features. + +Authentication and authorization complement each other: authentication verifies the user, while authorization governs the **extent of their access** within the system. + +----- +#### **Implementing Role-Based Access Control (RBAC)** +RBAC is a widely used strategy to **manage user permissions efficiently** by assigning roles to users. It simplifies authorization by grouping permissions under specific roles. + +- **Assigning roles to users:** + Users are given predefined roles such as **admin, editor, or viewer**. Each role corresponds to a set of permissions. For example: + - **Admin**: Full access to all operations, including creating, editing, and deleting content. + - **Editor**: Can modify content but lacks access to administrative settings. + - **Viewer**: Read-only access to content. +- **Defining permissions for each role:** + Each role has specific permissions that determine **what resources the user can access** and **what actions they can perform**. For example: + - Admins can manage other users and change system configurations. + - Editors can update or delete content but cannot manage users. +- **Importance of clear access rules:** + - Well-defined access rules **reduce the risk of security loopholes**. For example, without proper role separation, unauthorized users could gain access to sensitive data or critical system operations. + - **Consistency** in applying roles and permissions ensures better maintainability. Centralized control via middleware helps avoid inconsistencies, preventing scenarios where users have unintended privileges. + +RBAC simplifies managing access, especially in applications with many users. By grouping permissions under roles, developers can ensure that **user privileges are clear and manageable**, helping to maintain a secure system. + + +# **Node.js User Authentication Prerequisites** +To set up a secure user [authentication system in Node.js](https://supertokens.com/blog/how-to-deploy-supertokens-with-react-nodejs-express-on-vercel), you’ll need several essential tools and packages. These will enable you to handle routing, password encryption, token management, and cookie parsing, laying the groundwork for a robust authentication flow. + +#### **Setting Up the Environment** + +Install the following packages + +```bash +npm init -y +npm install express bcrypt jsonwebtoken cookie-parser +``` + +Run the server with the following command + +```bash +node server.js +``` + +----- +#### **Choosing an Authentication Method** + +There are several popular methods for handling authentication in web applications. Here’s an overview of the most commonly used approaches: + +- **JWT (JSON Web Tokens)**: JWTs are used for **stateless authentication**, meaning the server doesn’t store session information. Instead, a token is generated when the user logs in, which includes encoded user data and is sent with each request. The server then verifies the token to authenticate the user, making JWTs ideal for scalable, stateless applications. +- **Session-based Authentication**: This method involves storing session data on the server to keep track of user activity. When a user logs in, a session is created on the server, and a unique session ID is sent to the user’s browser, often in a cookie. This session ID is then used to identify the user for future requests. Session-based authentication is commonly used in applications where maintaining user state is essential. +- **OAuth**: OAuth is often used for third-party login, allowing users to sign in with existing accounts from services like Google, Facebook, or GitHub. It simplifies user login by leveraging these providers for authentication, eliminating the need for users to create new credentials for each application. OAuth is widely used in applications that prioritize user convenience and wish to offer social login options. + + +#### **Setting Up the Server and Authentication Routes** + +To configure the server, connect to MongoDB, and set up authentication routes, write the following code in the `Index.js` file: +### **Index.js** + +```typescript +const express = require('express'); +const mongoose = require('mongoose'); +const cors = require('cors'); +const dotenv = require('dotenv'); +const Message = require('./models/Message'); +const authRoutes = require('./routes/auth'); +const auth = require('./middleware/auth'); + +dotenv.config(); +const app = express(); +const PORT = process.env.PORT || 5000; + +app.use(cors()); +app.use(express.json()); + + + +// Routes +app.use('/api/auth', authRoutes); + +// Protected route (only accessible with a valid token) +app.get('/api/private', auth, (req, res) => { + res.send('This is a protected route'); + }); + + + +app.get('/', (req, res) => { + res.send('Hello World!'); +}); + +app.post('/message', async (req, res) => { + const newMessage = new Message({ text: req.body.text }); + try{ + await newMessage.save(); + res.status(201).json(newMessage); + } catch (error) { + res.status(500).json({ error: 'Error saving message' }); + } +}) + + +mongoose + .connect(process.env.MONGO\_URI) + .then(() => console.log('MongoDB Connected')) + .catch((err) => console.log('MongoDB connection error:', err)); + +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); +``` + +# **How to Implement User Authentication in Node.js** +## **Backend Implementation** +### **Step 1: User Registration Process** +To securely register users, start by creating a Mongoose schema with fields for name, email, and password. Then, hash the password with `bcrypt` before saving it to enhance security. Finally, include basic validation to ensure required fields and unique email entries. To implement this setup, write the following code: + +### **userModel.js/Models** +```typescript +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 }, + }); + +// Hash password before saving +userSchema.pre('save', async function (next) { + if (!this.isModified('password')) return next(); + this.password = await bcrypt.hash(this.password, 10); + next(); + }); + +module.exports = mongoose.model('User', userSchema); +``` + +### **Step 2: Login and Session Management** +To set up a secure login flow, validate user credentials by checking the stored hashed password in the database. JWT tokens will be used to maintain stateless sessions and manage token expiration, adding an extra layer of security. To implement this, write the following code in `Auth.js/Routes`: +### **Auth.js/Routes** +```typescript +const express = require('express'); +const User = require('../models/User'); +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcryptjs'); +const router = express.Router(); + + +// Register a new user +router.post('/register', async (req, res) => { + const {name, email, password} = req.body; + try{ + const newUser = new User({ name, email, password }); + await newUser.save(); + res.status(201).json({message: 'User registered successfully' }); + } catch (error) { + res.status(400).json({ error: 'User already exists' }); + } +}); + + +//Login user and generate JWT token +router.post('/login', async (req, res) => { + const { email, password } = req.body; + try{ + const user = await User.findOne({ email }); + if (!User) return res.status(404).json({ error: 'User not found'}); + + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) return res.status(401).json({ error: 'Invalid credentials' }); + + const token = jwt.sign({ userId: user.\_id }, process.env.JWT\_SECRET, { expiresIn: '1h'}); + res.json({ token }); + } catch (error) { + res.status(500).json({ error: 'Server error' }); + } +}); + +module.exports = router; +``` +### **Step 3: Protecting Routes with Middleware** +To restrict access to certain routes, middleware is used to ensure only authenticated users can access specific pages or data. The following code demonstrates a middleware function that checks for a valid token, verifies it, and grants access if authenticated. Write this code in `Auth.js/Middleware`: +### **Auth.js/Middleware** +```typescript +const jwt = require('jsonwebtoken'); + +const auth = (req, res, next) => { + const token = req.header('Authorization')?.split(' ')[1]; + if (!token) return res.status(401).json({ error: 'Access denied. No token provided.' }); + + try { + const decode = jwt.verify(token, process.env.JWT\_SECRET); + req.user = decode; + next(); + } catch (error) { + res.status(400).json({ error: 'Invalid Token' }); + } +}; + +module.exports = auth; +``` +This middleware function `auth` verifies if the request contains a valid JSON Web Token (JWT) in the authorization header. It checks for the token, verifies it using the secret key, and either allows access by calling `next()` or denies access if the token is invalid or missing. + +For implementing registration and login with JWT generation, add the following routes in `Auth.js/Routes`: +### **Auth.js/Routes** +```typescript +const express = require('express'); +const User = require('../models/User'); +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcryptjs'); +const router = express.Router(); + + +// Register a new user +router.post('/register', async (req, res) => { + const {name, email, password} = req.body; + try{ + const newUser = new User({ name, email, password }); + await newUser.save(); + res.status(201).json({message: 'User registered successfully' }); + } catch (error) { + res.status(400).json({ error: 'User already exists' }); + } +}); + + +//Login user and generate JWT token +router.post('/login', async (req, res) => { + const { email, password } = req.body; + try{ + const user = await User.findOne({ email }); + if (!User) return res.status(404).json({ error: 'User not found'}); + + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) return res.status(401).json({ error: 'Invalid credentials' }); + + const token = jwt.sign({ userId: user.\_id }, process.env.JWT\_SECRET, { expiresIn: '1h'}); + res.json({ token }); + } catch (error) { + res.status(500).json({ error: 'Server error' }); + } +}); + +module.exports = router; +``` +## **Frontend Implementation** +Finally, to create a simple login form that authenticates users, use the following code: +### **Login.js** +```typescript +import React, { useState } from 'react'; +import axios from 'axios'; + +function Login({ onLogin }) { // Accept onLogin as a prop + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [message, setMessage] = useState(''); + + const handleSubmit = async (e) => { + e.preventDefault(); // Prevents the page from refreshing on form submission + + try { + const { data } = await axios.post('http://localhost:5000/api/auth/login', { + email, + password + }); + localStorage.setItem('token', data.token); // Store JWT in localStorage + setMessage('Login Successful'); + onLogin(); // Call onLogin to inform the App component of the login status + } catch (error) { + setMessage('Login failed. Check your credentials.'); + } + }; + + return ( +
{message}
+