Node.js API: JWT Authentication
JSON Web Tokens (JWT) are a popular standard for creating access tokens to be used in identity verification. They are compact, URL-safe, and are typically used in web applications for authentication and authorization. This tutorial will guide you through implementing JWT authentication in your Node.js API using Express and popular libraries.
What is JWT?
JWT is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs are commonly used for:
- Authentication: After a user logs in, a server can issue a JWT, which the user then uses to prove their identity on subsequent requests.
- Authorization: JWTs can carry permissions and roles, allowing the server to decide if a user is authorized to perform a specific action.
- Information Exchange: JWTs can securely transmit information between parties.
Structure of a JWT
A JWT looks like this: xxxxx.yyyyy.zzzzz
The three parts are:
- Header: Contains metadata about the token, such as the algorithm used for signing (e.g., HMAC SHA256 or RSA) and the token type (JWT).
- Payload: Contains the claims. Claims are statements about an entity (typically, the user) and additional data. Common claims include
iss(issuer),exp(expiration time),sub(subject), and custom claims like user ID or roles. - Signature: Used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn't changed along the way. It's created by taking the encoded header, the encoded payload, a secret (or public/private key pair), and signing it with the algorithm specified in the header.
Implementing JWT in Node.js
We'll use the jsonwebtoken package, which is a popular choice for handling JWTs in Node.js. First, install it:
npm install jsonwebtoken express-validator bcryptjs
We also include express-validator for request validation and bcryptjs for password hashing, which is crucial for security.
1. Setup and Configuration
Create a configuration file (e.g., config/jwtConfig.js) to store your JWT secret and expiration time.
// config/jwtConfig.js
module.exports = {
jwtSecret: process.env.JWT_SECRET || 'your_super_secret_key_replace_me',
jwtExpirationTime: '1h' // Token expires in 1 hour
};
Make sure to set JWT_SECRET as an environment variable in your production environment.
2. User Registration
When a user registers, hash their password before storing it in the database.
// controllers/authController.js
const bcrypt = require('bcryptjs');
const User = require('../models/User'); // Assuming you have a User model
exports.register = async (req, res) => {
const { username, email, password } = req.body;
try {
// Check if user already exists
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({ msg: 'User already exists' });
}
// Hash password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
// Create new user
user = new User({
username,
email,
password: hashedPassword
});
await user.save();
res.status(201).json({ msg: 'User registered successfully' });
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
};
3. User Login and Token Generation
When a user logs in, verify their credentials and, if valid, generate a JWT.
// controllers/authController.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const User = require('../models/User');
const { jwtSecret, jwtExpirationTime } = require('../config/jwtConfig');
exports.login = async (req, res) => {
const { email, password } = req.body;
try {
// Check if user exists
let user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ msg: 'Invalid Credentials' });
}
// Validate password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ msg: 'Invalid Credentials' });
}
// Create JWT Payload
const payload = {
user: {
id: user.id,
username: user.username,
email: user.email
}
};
// Sign Token
jwt.sign(payload, jwtSecret, { expiresIn: jwtExpirationTime }, (err, token) => {
if (err) throw err;
res.json({ token });
});
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
};
4. Middleware for Protected Routes
Create middleware to protect routes that require authentication. This middleware will verify the JWT sent in the Authorization header.
// middleware/authMiddleware.js
const jwt = require('jsonwebtoken');
const { jwtSecret } = require('../config/jwtConfig');
module.exports = function(req, res, next) {
// Get token from header
const token = req.header('x-auth-token');
// Check if no token
if (!token) {
return res.status(401).json({ msg: 'No token, authorization denied' });
}
// Verify token
try {
const decoded = jwt.verify(token, jwtSecret);
req.user = decoded.user; // Attach user info to request
next();
} catch (err) {
res.status(401).json({ msg: 'Token is not valid' });
}
};
5. Applying the Middleware
Use the authMiddleware in your routes to protect specific endpoints.
// routes/api/protectedRoutes.js
const express = require('express');
const router = express.Router();
const auth = require('../../middleware/authMiddleware');
// Example protected route
router.get('/profile', auth, (req, res) => {
res.json(req.user); // Returns the user object attached by the middleware
});
module.exports = router;
Best Practices
- Keep Secrets Secure: Never hardcode your JWT secret. Use environment variables.
- Short Expiration: Set reasonable expiration times for tokens to minimize the impact of a leaked token. Consider implementing refresh tokens for longer-lived sessions.
- HTTPS: Always use HTTPS to prevent tokens from being intercepted in transit.
- Payload Content: Do not store sensitive information directly in the JWT payload, as it is only base64 encoded, not encrypted.
- Token Invalidation: JWTs are stateless, meaning they cannot be easily revoked once issued. If you need immediate revocation (e.g., for password changes), you'll need to implement a blacklist or token management system.
Conclusion
JWT authentication provides a robust and scalable way to secure your Node.js APIs. By understanding its structure and implementing best practices, you can effectively manage user authentication and authorization in your applications.
This tutorial covered the basics of JWTs, their structure, and a practical implementation using Express and the jsonwebtoken library. Continue exploring by implementing refresh tokens and advanced security measures for production environments.
Next Steps:
- Explore refresh tokens for managing longer-lived sessions.
- Learn about OAuth 2.0 for delegated authorization.
- Implement proper error handling and logging.