Passport.js, when used with JWTs, doesn’t actually authenticate your Express API in the traditional sense; it delegates the task of verifying the JWT’s signature and claims to a separate strategy, making your API endpoints the ultimate gatekeepers.

Let’s see this in action. Imagine a simple Express app with a protected route:

const express = require('express');
const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;

const app = express();

// Passport JWT Strategy setup
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken(); // How to get the token from the request
opts.secretOrKey = 'supersecret'; // The secret key used to sign the JWT

passport.use(new JwtStrategy(opts, (jwtPayload, done) => {
    // In a real app, you'd fetch the user from your DB based on jwtPayload.sub
    // For this example, we'll just simulate a user lookup
    const user = { id: jwtPayload.sub, username: 'testuser' };
    if (user) {
        return done(null, user); // Authentication successful
    } else {
        return done(null, false); // User not found
    }
}));

// Middleware to initialize Passport
app.use(passport.initialize());

// A public route
app.get('/public', (req, res) => {
    res.send('This is a public route.');
});

// A protected route
app.get('/protected', passport.authenticate('jwt', { session: false }), (req, res) => {
    res.json({ message: 'This is a protected route!', user: req.user });
});

// Route to get a token (for demonstration)
app.post('/login', (req, res) => {
    // In a real app, you'd verify username/password here
    const user = { id: 'user123', username: 'testuser' };
    const token = jwt.sign({ sub: user.id }, 'supersecret', { expiresIn: '1h' });
    res.json({ token });
});

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

In this setup, passport.authenticate('jwt', { session: false }) is the key. When a request hits /protected, Passport intercepts it. It then hands off the request to the JwtStrategy. The strategy, configured with ExtractJwt.fromAuthHeaderAsBearerToken(), looks for a Authorization: Bearer <token> header. If found, it takes the token and, using the provided secretOrKey, verifies its signature. If the signature is valid, it decodes the payload (like jwtPayload.sub) and passes it to the callback function. This callback then simulates finding a user. If a user is found, done(null, user) is called, and Passport attaches the user object to req.user. If done is called with false or an error, the request is rejected. Crucially, session: false tells Passport not to create a session, meaning each request must carry its own valid JWT.

The core problem this pattern solves is stateless authentication. Instead of the server needing to maintain a session for every logged-in user (which scales poorly), the client stores a token, and the server verifies that token on each request. The token itself contains the necessary information (like user ID) and proof of its authenticity (the signature). This makes your API horizontally scalable because any server instance can authenticate any request without needing shared session state.

The mental model here is that JWTs are self-contained credentials. They have a header (algorithm, type), a payload (claims like user ID, expiration), and a signature. The signature is generated using the header, payload, and a secret key. Anyone with the secret key can verify the signature. This means the server doesn’t need to look up anything in a database to verify the token’s integrity; it only needs to do so if the token’s payload indicates a need to fetch associated user data for authorization.

What most people miss is that the secretOrKey is not just for signing; it’s also for verification. If you change the secretOrKey on the server, all existing JWTs become invalid because their signatures can no longer be verified. This is why rotating secrets is a complex operation if you don’t have a mechanism to support multiple valid secrets simultaneously during the transition.

The next step after implementing JWT authentication is often handling authorization – determining what an authenticated user is allowed to do.

Want structured learning?

Take the full Express course →