JWTs
In-depth guide on implementing JWT-based authentication in ExpressJS, including token vs JWT comparison, JWT creation, verification, secure practices, middleware handling, and a full example site structure.
JSON Web Token (JWT) Authentication in ExpressJS
In modern web applications, JSON Web Tokens (JWT) are a popular choice for implementing authentication due to their standardized structure, ease of use, and ability to carry user information in a secure way.
Tokens vs JWT
| Feature | Simple Token | JWT |
|---|---|---|
| Format | Random string (opaque) | Structured JSON object, encoded as Base64 |
| Information Carried | None (server tracks state) | Encoded payload (user ID, roles, etc.) |
| Self-Contained | No (requires server lookup) | Yes (payload carries info) |
| Verification | Check token existence in storage | Verify signature using secret |
| Decodable by Anyone | No meaningful info | Base64-decoded easily |
| Expiration | Manual implementation | Built-in exp field supported |
Why JWT Is Secure Despite Being Decodable
- The payload of a JWT is Base64-encoded, not encrypted, so anyone can decode and read it.
- The security lies in the signature, not the payload secrecy.
- The signature is created by hashing the header + payload with a secret key.
- Any tampering of the payload invalidates the signature and causes verification to fail.
Replacing Token Logic with JWT
Step 1: Install Dependencies
npm install jsonwebtokenStep 2: Generating a JWT
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'supersecretkey';
app.post('/login', (req, res) => {
const { username, password } = req.body;
const validUser = { username: 'admin', password: 'password123' };
if (username === validUser.username && password === validUser.password) {
const payload = { username };
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' });
res.json({ token, message: 'Login successful' });
} else {
res.status(401).json({ message: 'Invalid credentials' });
}
});Step 3: Authentication Middleware with JWT
function authenticateJWT(req, res, next) {
const token = req.headers['authorization'];
if (!token) {
return res.status(401).json({ message: 'Token is required.' });
}
// Always use jwt.verify instead of manual decoding
jwt.verify(token, JWT_SECRET, (err, decoded) => {
if (err) {
return res.status(403).json({ message: 'Invalid or expired token.' });
}
// Store decoded payload once for downstream handlers
req.user = decoded;
next();
});
}Why We Should Not Manually Decode JWT
- Manual decoding (
jwt.decode()) does not verify the signature, so it is completely insecure for authentication purposes. - Use
jwt.verify()to ensure the token is both valid and untampered. - The middleware ensures verification happens only once per request.
Step 4: Protected Route Example
app.get('/profile', authenticateJWT, (req, res) => {
res.json({ message: `Hello, ${req.user.username}. This is your profile.` });
});Step 5: Logout (Optional)
With JWT being stateless, the common approach is to have the frontend delete the token upon logout.
If needed, implement a blacklist:
const blacklistedTokens = new Set();
function authenticateJWT(req, res, next) {
const token = req.headers['authorization'];
if (blacklistedTokens.has(token)) {
return res.status(403).json({ message: 'Token has been revoked.' });
}
jwt.verify(token, JWT_SECRET, (err, decoded) => {
if (err) return res.status(403).json({ message: 'Invalid or expired token.' });
req.user = decoded;
next();
});
}
app.post('/logout', authenticateJWT, (req, res) => {
blacklistedTokens.add(req.headers['authorization']);
res.json({ message: 'Logged out successfully.' });
});Middleware Efficiency Tip
Rather than verifying the token multiple times across handlers, verify it once in middleware and attach the decoded user data to req.user. All downstream code can trust req.user.
Example:
function authenticateJWT(req, res, next) {
const token = req.headers['authorization'];
if (!token) return res.status(401).json({ message: 'Token is required.' });
jwt.verify(token, JWT_SECRET, (err, decoded) => {
if (err) return res.status(403).json({ message: 'Invalid or expired token.' });
req.user = decoded; // Modify request object here
next();
});
}Example Site Structure (Simple Frontend + Backend)
Frontend (React Example)
// Login Form (React Component)
function Login() {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const handleLogin = async () => {
const res = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
const data = await res.json();
if (data.token) {
localStorage.setItem('token', data.token);
}
};
return (
<div>
<input onChange={(e) => setUsername(e.target.value)} placeholder="Username" />
<input type="password" onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
<button onClick={handleLogin}>Login</button>
</div>
);
}Backend (Express + JWT)
app.post('/login', ...); // Generates JWT on login
app.get('/profile', authenticateJWT, (req, res) => { // Protected route
res.json({ username: req.user.username });
});Frontend will send the token in the Authorization header when calling /profile.
Important Notes
- Never rely on
jwt.decode()for verification. Always usejwt.verify(). - Attach the decoded user payload to
req.userin middleware for downstream use. - Use HTTPS in production to secure token transmission.
- Set short expiration and use refresh tokens for better security.
Authentication
Guide on implementing basic Token-based Authentication in ExpressJS, covering Auth Workflow, example workflow, and creating authenticated endpoints using a custom generateToken approach.
Local Storage
Comprehensive guide to using Local Storage in web applications, including how it works, use cases, methods, best practices, and security considerations.