+5

🔐JSON Web Tokens (JWT) for Secure Authentication and Authorization in Node.js Express

Introduction to JSON Web Tokens (JWT)

JSON Web Tokens (JWT) is an open standard (RFC 7519) that defines a compact and self-contained method for securely transmitting information between parties as a JSON object. JWTs are particularly useful for authentication and authorization, as they allow a server to verify a client's identity and grant access to protected resources based on the client's claims or permissions.

In this article, we will explore the use of JWT for secure authentication and authorization in a Node.js Express application. We will discuss the following topics:

  1. How JWT works
  2. Setting up a Node.js Express application
  3. Implementing JWT-based authentication
  4. Implementing JWT-based authorization
  5. Best practices and security considerations

How JWT Works

Structure of a JWT

A JSON Web Token consists of three parts: the header, the payload, and the signature. These three parts are base64Url encoded, concatenated with a period (.) separator, and form the complete JWT as a string. The structure of a JWT is as follows:

header.payload.signature

Header

The header typically contains two properties:

  • alg: The signing algorithm being used, such as HMAC SHA256 (HS256) or RSA (RS256).
  • typ: The token's type, usually set to "JWT".

A sample header in JSON format:

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

The payload contains the claims, which are statements about the subject (e.g., user) and additional metadata. There are three types of claims:

  • Registered claims: Predefined claims such as iss (issuer), exp (expiration time), sub (subject), and aud (audience).
  • Public claims: Custom claims agreed upon by both parties. To avoid collisions, they should be registered in the IANA JSON Web Token Registry or use a collision-resistant naming convention.
  • Private claims: Custom claims used between the two parties and not intended for public consumption.

A sample payload in JSON format:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Signature

The signature is used to verify the integrity of the token. It is generated by combining the encoded header, the encoded payload, a secret, and the algorithm specified in the header. For example, with the HMAC SHA256 algorithm:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

Token Verification Process

When a client sends a JWT to the server, the server verifies the token's signature by decoding the JWT and recalculating the signature using the same secret or private key used during token creation. If the recalculated signature matches the one in the JWT, the server can trust the token's contents.

Setting Up a Node.js Express Application

To start, we need to set up a Node.js Express application. First, ensure you have Node.js and npm installed. Then, create a new directory for the project and initialize it with npm init. After answering the prompts, install the required dependencies:

npm install express jsonwebtoken bcryptjs body-parser dotenv

Create an .env file to store sensitive information, such as the JWT secret and the password salt rounds:

JWT_SECRET=my_jwt_secret
SALT_ROUNDS=10

Implementing JWT-based Authentication

User Registration

In this example, we will use a simple in-memory storage for user data. In a production environment, you would typically use a database for persistent storage. First, create a file named users.js with the following content:

const users = [];

module.exports = users;

Next, create an authController.js file to handle user registration and authentication. Import the required dependencies and the users array:

const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const dotenv = require('dotenv');
const users = require('./users');

dotenv.config();

Now, create a function to handle user registration. This function will hash the user's password using bcryptjs and store the user's information in the users array:

const register = async (req, res) => {
  const { username, password } = req.body;

  // Check if the user already exists
  const userExists = users.find((user) => user.username === username);
  if (userExists) {
    return res.status(400).send('User already exists');
  }

  // Hash the password
  const salt = await bcrypt.genSalt(parseInt(process.env.SALT_ROUNDS));
  const hashedPassword = await bcrypt.hash(password, salt);

  // Store the user
  const newUser = { username, password: hashedPassword };
  users.push(newUser);

  res.status(201).send('User registered successfully');
};

User Authentication

Create a function to authenticate users by comparing the submitted password with the stored hash. If the password is correct, generate a JWT and return it to the client:

const authenticate = async (req, res) => {
  const { username, password } = req.body;

  // Find the user
  const user = users.find((user) => user.username === username);
  if (!user) {
    return res.status(404).send('User not found');
  }

  // Verify the password
  const isPasswordValid = await bcrypt.compare(password, user.password);
  if (!isPasswordValid) {
    return res.status(401).send('Invalid credentials');
  }

  // Create a JWT
  const token = jwt.sign({ username: user.username }, process.env.JWT_SECRET);

  res.status(200).json({ token });
};

Finally, export the register and authenticate functions:

module.exports = {
  register,
  authenticate,
};

Implementing JWT-based Authorization

Create a middleware function in a new file named authMiddleware.js to verify the JWT in incoming requests:

const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');

dotenv.config();

const verifyToken = (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader) {
    return res.status(401).send('Access denied: No token provided');
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    res.status(400).send('Invalid token');
  }
};

module.exports = verifyToken;

Now you can use this middleware to protect your routes. For example, create a protected route in a new file named routes.js:

const express = require('express');
const router = express.Router();
const verifyToken = require('./authMiddleware');

router.get('/protected', verifyToken, (req, res) => {
  res.send('Access granted: You are authenticated');
});

module.exports = router;

Wiring Up the Application Create an app.js file to wire up the application. Import the required dependencies, the authentication controller, and the routes:

const express = require('express');
const bodyParser = require('body-parser');
const authController = require('./authController');
const routes = require('./routes');

const app = express();
const port = process.env.PORT || 3000;

Set up the Express middleware, register the authentication routes, and use the protected routes:

app.use(bodyParser.json());

// Authentication routes
app.post('/register', authController.register);
app.post('/authenticate', authController.authenticate);

// Protected routes
app.use('/', routes);

Start the Express server:

app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

Now, you can run the application with node app.js and use an API client like Postman to test the /register, /authenticate, and /protected endpoints.

Best Practices and Security Considerations

  1. Store the JWT secret securely: Use environment variables, a secrets manager, or a configuration management tool to store the JWT secret securely.
  2. Use HTTPS: To protect JWTs from being intercepted during transmission, always use HTTPS for communication between the client and server.
  3. Set an appropriate expiration time: Keep the JWT's lifetime short to reduce the risk of misuse. You can set the exp claim to an appropriate value when creating the JWT.
  4. Handle token revocation: Implement a mechanism to revoke tokens, such as using a token blacklist or implementing a token introspection endpoint.
  5. Validate input: Always validate user input on both the client and server sides to prevent injection attacks and other vulnerabilities.
  6. Implement proper error handling: Properly handle errors and avoid disclosing sensitive information in error messages.

Conclusion

In this article, we explored how to use JSON Web Tokens for secure authentication and authorization in a Node.js Express application. We discussed the structure of JWTs, implemented user registration and authentication, and protected routes using JWT-based authorization middleware. Additionally, we covered best practices and security considerations to ensure the secure use of JWTs in your application.

Mình hy vọng bạn thích bài viết này và học thêm được điều gì đó mới.

Donate mình một ly cafe hoặc 1 cây bút bi để mình có thêm động lực cho ra nhiều bài viết hay và chất lượng hơn trong tương lai nhé. À mà nếu bạn có bất kỳ câu hỏi nào thì đừng ngại comment hoặc liên hệ mình qua: Zalo - 0374226770 hoặc Facebook. Mình xin cảm ơn.

Momo: NGUYỄN ANH TUẤN - 0374226770

TPBank: NGUYỄN ANH TUẤN - 0374226770 (hoặc 01681423001)

image.png


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.