HVRDHVRD
ExpressJS

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

FeatureSimple TokenJWT
FormatRandom string (opaque)Structured JSON object, encoded as Base64
Information CarriedNone (server tracks state)Encoded payload (user ID, roles, etc.)
Self-ContainedNo (requires server lookup)Yes (payload carries info)
VerificationCheck token existence in storageVerify signature using secret
Decodable by AnyoneNo meaningful infoBase64-decoded easily
ExpirationManual implementationBuilt-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 jsonwebtoken

Step 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 use jwt.verify().
  • Attach the decoded user payload to req.user in middleware for downstream use.
  • Use HTTPS in production to secure token transmission.
  • Set short expiration and use refresh tokens for better security.