Why JWT Tokens Should Always Be Sent Over HTTPS

All that work building a solid JWT auth system — strong key, proper expiry, refresh tokens — means nothing if the token travels across the internet in plain text.

Imagine writing your house key on a postcard and mailing it to someone. Anyone who handles that postcard along the way can read it, copy it, and let themselves in.

That's exactly what sending a JWT over HTTP looks like.

The token travels in plain text. Anyone sitting on the same network can see it, copy it, and use it — no hacking required.

What Actually Happens Over HTTP

HTTP sends data as plain text across the network. Every router, switch, and server that handles your request sees everything — the URL, the headers, the body, and yes, the Authorization header with your JWT token inside it.

This is called a man-in-the-middle attack. Someone positions themselves between your user and your server and quietly reads everything that passes through.

On public Wi-Fi — coffee shops, airports, hotels — this is genuinely easy to do. Tools exist that make it almost trivially simple to capture network traffic. A JWT token in an HTTP request is a gift to anyone running one of those tools.

They get the token. They use it. Your server can't tell the difference between the real user and the attacker because the token is valid.

What HTTPS Actually Does

HTTPS wraps your traffic in TLS encryption before it leaves the device. The data is scrambled and can only be unscrambled by the intended server.

Someone intercepting an HTTPS request sees gibberish. Not the URL. Not the headers. Not the token. Just encrypted noise they can't do anything with.

That's it. That's the whole reason HTTPS exists. To protect data in transit from people who shouldn't see it.

Your JWT token is exactly the kind of data that needs protecting in transit.

The Signature Doesn't Help Here

A common misconception worth clearing up: "But the token has a signature. Doesn't that protect it?"

The signature proves the token hasn't been modified. It doesn't stop someone from copying it and using it as-is.

An attacker who intercepts your token over HTTP doesn't need to change it. They just need to replay it — send the exact same token to your server pretending to be you. The signature is still valid. Your server accepts it. They're in.

Signature = tamper protection. HTTPS = theft protection. You need both.

How to Force HTTPS in Node.js

The simplest way is to redirect any HTTP requests straight to HTTPS before they go anywhere near your application logic:

const express = require('express'); const app = express(); // Redirect HTTP to HTTPS in production app.use((req, res, next) => { if ( process.env.NODE_ENV === 'production' && req.headers['x-forwarded-proto'] !== 'https' ) { return res.redirect(301, `https://${req.headers.host}${req.url}`); } next(); });

The x-forwarded-proto header is set by most hosting platforms and reverse proxies to tell your app whether the original request came in over HTTP or HTTPS. Check it and redirect if it's not HTTPS.

Put this as the very first middleware in your app, before routes, before auth, before everything.

Set the Secure Flag on Cookies

If you store your JWT (or refresh token) in a cookie, there's a specific flag that tells the browser to only send the cookie over HTTPS:

res.cookie('refreshToken', token, { httpOnly: true, // JS can't read it secure: true, // Only sent over HTTPS — never HTTP sameSite: 'strict', // Only sent to your own domain maxAge: 7 * 24 * 60 * 60 * 1000 });

With secure: true, the browser will simply refuse to attach this cookie to any HTTP request. The cookie only travels on HTTPS connections.

If you forget this flag and your site ever accidentally serves a page over HTTP, the cookie won't be exposed. One flag, meaningful protection.

Add HTTP Strict Transport Security (HSTS)

HSTS is a response header that tells browsers: "Never visit this site over HTTP again. Always use HTTPS, no matter what."

Once a browser sees this header, it will automatically upgrade any future HTTP requests to HTTPS — before they even leave the device. The token never has a chance to travel unencrypted.

// Add HSTS header to all responses app.use((req, res, next) => { res.setHeader( 'Strict-Transport-Security', 'max-age=31536000; includeSubDomains' // 1 year ); next(); }); // Or use the helmet package — handles this and more const helmet = require('helmet'); app.use(helmet());

The helmet package is worth adding to any Express app. It sets a bunch of security headers including HSTS, all in one line. No reason not to use it.

What About Local Development?

You don't need HTTPS on localhost. Local traffic doesn't leave your machine so there's nothing to intercept.

That's why the HTTPS redirect above checks for process.env.NODE_ENV === 'production' first. In development, HTTP works fine. In production, everything goes over HTTPS.

The secure: true cookie flag is the one exception — it blocks cookies on localhost too since localhost isn't HTTPS. The standard workaround is to only set secure: true in production:

res.cookie('refreshToken', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', // HTTPS in prod, HTTP in dev sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 });

Development stays smooth. Production stays secure. Simple toggle.

The Full Security Picture

HTTPS protects tokens in transit. But it's one layer in a stack, not the whole thing.

Here's what a properly secured JWT setup looks like end to end:

  • Strong secret key — generated properly, not typed by hand
  • Secret stored in environment variables — never in code or Git
  • Short access token expiry — limits damage if a token is somehow stolen
  • HttpOnly cookies for refresh tokens — can't be read by JavaScript
  • secure: true on cookies — won't travel over HTTP
  • HTTPS everywhere in production — encrypts all traffic in transit
  • HSTS header — prevents browsers from accidentally using HTTP

Each layer handles a different threat. HTTPS handles interception. Short expiry handles stolen tokens. HttpOnly handles XSS. Strong keys handle brute force.

Skip any one of them and you have a gap. Together they cover all the common attack surfaces.

The strong secret key part is where I always start. I generate it at jwtsecretkeygenerator.com before writing a single line of auth code. Everything else gets built on top of that foundation — including making sure every request that carries that token travels over HTTPS.

💡 One Rule: If your app handles real users, real data, or real logins — HTTPS is not optional. Free SSL certificates from Let's Encrypt are available on basically every hosting platform. There's no cost reason to skip it anymore. Just turn it on.