DEV Community

Stack Horizon
Stack Horizon

Posted on

JWT Auth Without the Confusion

The Problem with JWT Tutorials

Most JWT tutorials dump a ton of theory on you: signatures, algorithms, refresh tokens, and where to store them. By the end, you're more confused than when you started. I've been there. So let's strip it down to what actually matters and build a simple, working JWT auth flow.

What JWT Actually Is

A JSON Web Token is just a string with three parts separated by dots:

header.payload.signature
Enter fullscreen mode Exit fullscreen mode
  • Header: tells the server what algorithm was used to sign it (usually HS256).
  • Payload: contains claims like userId, exp, and iat.
  • Signature: a hash of the header and payload using a secret key. This is what prevents tampering.

The key point: the token is signed, not encrypted. Anyone can decode the payload, but they can't modify it without invalidating the signature.

The Flow in Plain English

  1. User logs in with username and password.
  2. Server verifies credentials and creates a token containing the user's ID and an expiration time.
  3. Server sends the token back to the client.
  4. Client stores the token (usually in memory or localStorage).
  5. Client sends the token in the Authorization header on every request.
  6. Server verifies the token's signature and expiration, then reads the user ID from the payload.

That's it. No sessions, no cookies, no server-side storage.

Minimal Node.js Implementation

Let's build a tiny Express server with two routes: /login and /profile. We'll use the jsonwebtoken package.

npm install express jsonwebtoken
Enter fullscreen mode Exit fullscreen mode

Here's the complete server:

const express = require('express');
const jwt = require('jsonwebtoken');

const app = express();
app.use(express.json());

const SECRET = 'your-secret-key';

// Dummy user database
const users = [{ id: 1, username: 'alice', password: 'secret' }];

// Login route
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = users.find(u => u.username === username && u.password === password);
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const token = jwt.sign({ userId: user.id }, SECRET, { expiresIn: '1h' });
  res.json({ token });
});

// Protected route
app.get('/profile', (req, res) => {
  const authHeader = req.headers.authorization;
  if (!authHeader) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const token = authHeader.split(' ')[1]; // Remove 'Bearer '
  try {
    const payload = jwt.verify(token, SECRET);
    const user = users.find(u => u.id === payload.userId);
    res.json({ username: user.username });
  } catch (err) {
    res.status(401).json({ error: 'Invalid or expired token' });
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));
Enter fullscreen mode Exit fullscreen mode

Client-Side Usage (Fetch)

On the client, after login, store the token and attach it to requests:

// Login
const res = await fetch('/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ username: 'alice', password: 'secret' })
});
const { token } = await res.json();
localStorage.setItem('token', token);

// Fetch profile
const profileRes = await fetch('/profile', {
  headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
});
const profile = await profileRes.json();
console.log(profile); // { username: 'alice' }
Enter fullscreen mode Exit fullscreen mode

Common Mistakes and How to Avoid Them

1. Storing Secrets in Client Code

Never put your JWT secret in the client. It's only used on the server to sign and verify. If it leaks, anyone can forge tokens.

2. Not Checking Expiration

The jsonwebtoken library checks exp automatically. If you roll your own, don't forget to validate it. A token that never expires is a security hole.

3. Putting Sensitive Data in the Payload

The payload is base64-encoded, not encrypted. Don't put passwords or credit card numbers in there. Just the user ID and maybe a role.

4. Using localStorage for High-Security Apps

localStorage is accessible to any JavaScript on the page, making it vulnerable to XSS. For highly sensitive apps, consider storing tokens in memory or using httpOnly cookies. For most tutorials and small projects, localStorage is fine.

When JWT Makes Sense

  • Stateless APIs: you don't want to manage session storage.
  • Microservices: each service can verify tokens independently.
  • Mobile apps: tokens work well with native clients.

When to Avoid It

  • Server-rendered apps with sessions: traditional cookies are simpler and more secure.
  • Short-lived, high-frequency requests: token verification adds CPU overhead, though it's usually negligible.

Final Thoughts

JWT is not magic. It's a signed piece of data that lets your server trust requests without storing session state. Once you understand the three parts and the simple flow, you can implement it in any language. Start small, test with a tool like Postman, and then expand to refresh tokens and logout blacklists if you need them.

Now go build something without the confusion.

Top comments (0)