JWT Authentication Explained for Beginners

JSON Web Tokens (JWTs) are the backbone of authentication in most modern REST APIs and single-page apps. They let a server verify who a user is without storing session state in a database. This guide breaks down exactly what's inside a JWT and how it flows through a login and API request.

The three parts of a JWT

A JWT is a single string made of three parts separated by dots: header.payload.signature. Each part is Base64url-encoded — a URL-safe variant of Base64 that swaps +// for -/_ and drops the padding.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJ1c2VySWQiOiI0MiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTc4MzY0ODAwMH0
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

// header:    { "alg": "HS256", "typ": "JWT" }
// payload:   { "userId": "42", "role": "admin", "exp": 1783648000 }
// signature: HMAC-SHA256(header + "." + payload, SECRET_KEY)

Why Base64url and not plain Base64?

JWTs are frequently passed in URLs, HTTP headers, and cookies. Standard Base64's + and / characters have special meaning in URLs and would need percent-encoding. Base64url avoids that entirely by using only URL-safe characters, so the token can be dropped into a header or query string with zero extra encoding.

function base64UrlEncode(obj) {
  const json = JSON.stringify(obj);
  const base64 = btoa(json);
  return base64
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, ''); // padding is dropped, not needed for JWTs
}

base64UrlEncode({ userId: '42', role: 'admin' });
// → 'eyJ1c2VySWQiOiI0MiIsInJvbGUiOiJhZG1pbiJ9'

The signature — what it actually protects

The signature is computed by hashing the header and payload together with a secret key known only to the server (for HMAC algorithms like HS256) or a private key (for RSA/ECDSA algorithms like RS256). It's important to understand what this does and doesn't do:

  • Protects against tampering — if anyone changes the payload (e.g. role: admin), the signature no longer matches and verification fails.
  • Does NOT hide the contents — the header and payload are just Base64url, not encrypted. Anyone can decode and read them without the secret key.
  • Never put sensitive data (passwords, credit card numbers) in a JWT payload — treat it as visible to the client and anyone who intercepts it.

The full authentication flow

  1. User submits email/password to POST /login.
  2. Server verifies credentials against the database.
  3. Server creates a JWT payload with claims (userId, role, exp) and signs it with a secret key, returning the token to the client.
  4. Client stores the token (memory, httpOnly cookie, or localStorage) and attaches it to every subsequent request: Authorization: Bearer <token>.
  5. On each request, the server verifies the signature and checks exp — no database session lookup needed, which is why JWTs scale well across stateless services.
  6. When the token expires, the client either re-authenticates or uses a longer-lived refresh token to obtain a new access token.
// Node.js server — issuing and verifying a JWT with jsonwebtoken
import jwt from 'jsonwebtoken';

// Issue a token at login
const token = jwt.sign(
  { userId: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: '1h' }
);

// Verify on protected routes
app.get('/api/profile', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    res.json({ userId: payload.userId });
  } catch (err) {
    res.status(401).json({ error: 'Invalid or expired token' });
  }
});

Common beginner mistakes

  • Using jwt.decode() instead of jwt.verify() for access control — decode does not check the signature at all.
  • Storing sensitive personal data in the payload, assuming it's hidden.
  • Never setting an expiry (exp), so a stolen token stays valid forever.
  • Storing the secret key in source control instead of an environment variable.

Frequently Asked Questions

What are the three parts of a JWT?

A JWT has three dot-separated parts: the header (algorithm and token type), the payload (claims/data like user ID and expiry), and the signature (used to verify the token was not tampered with). All three parts are Base64url-encoded.

Is a JWT encrypted?

No, by default a standard JWT is only Base64url-encoded and signed, not encrypted. Anyone can decode the header and payload and read the claims. The signature only proves the token has not been altered — it does not hide the contents. Use JWE if you need actual encryption.

How does JWT authentication work end to end?

The user logs in with credentials, the server verifies them and issues a signed JWT, the client stores the token and sends it in the Authorization header on future requests, and the server verifies the signature on each request to authenticate without a database lookup.

Try the Free Base64 Encoder/Decoder

Paste any JWT segment into our free tool to decode the header or payload instantly and inspect the raw claims — no library or code required.

Related articles