Hi, I'm Tuan, a Full-stack Web Developer from Tokyo 😊. Follow my blog to not miss out on useful and interesting articles in the future.
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:
- How JWT works
- Setting up a Node.js Express application
- Implementing JWT-based authentication
- Implementing JWT-based authorization
- 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), andaud
(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
- Store the JWT secret securely: Use environment variables, a secrets manager, or a configuration management tool to store the JWT secret securely.
- Use HTTPS: To protect JWTs from being intercepted during transmission, always use HTTPS for communication between the client and server.
- 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.
- Handle token revocation: Implement a mechanism to revoke tokens, such as using a token blacklist or implementing a token introspection endpoint.
- Validate input: Always validate user input on both the client and server sides to prevent injection attacks and other vulnerabilities.
- 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.
And Finally
As always, I hope you enjoyed this article and got something new. Thank you and see you in the next articles!
If you liked this article, please give me a like and subscribe to support me. Thank you. 😊