Articles

JWT Authentication: A Comprehensive Guide to Best Practices

Master the fundamentals of JSON Web Tokens, their role in OAuth 2.0 and OIDC, and the security considerations necessary for modern authentication. Learn when to implement JWTs versus traditional server-side sessions.

Written by:
APin

Senior Technology Analyst • Verified Expert

More from this author
JWT Authentication: A Comprehensive Guide to Best Practices

Master the fundamentals of JSON Web Tokens, their role in OAuth 2.0 and OIDC, and the security considerations necessary for modern authentication. Learn when to implement JWTs versus traditional server-side sessions.

Understanding JSON Web Tokens (JWT)

A JSON Web Token (JWT) is a compact, URL‑safe string that carries a set of claims between two parties. Its core structure consists of two Base64URL‑encoded JSON objects separated by a period (.):

(Header).(Payload)

The JOSE header describes how the token is processed. At minimum it contains:

  • "alg" – the algorithm used for signing (e.g., HS256) or "none" when no integrity protection is applied.
  • "typ" – typically set to "JWT" to identify the token type.

Example header (JSON):

{
  "typ": "JWT",
  "alg": "HS256"
}

The payload (or claims) holds the data the token conveys, such as user identifiers, roles, or expiration timestamps. Claims are plain JSON key‑value pairs:

{
  "id": "1234567890",
  "name": "John Doe",
  "age": 36
}

Both header and payload are UTF‑8 encoded, then Base64URL‑encoded to ensure URL safety. An unsecured token—where "alg":"none"—looks like:

eyJhbGciOiJub25lIn0.eyJpZCI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZ2UiOjM2fQ

Because there is no cryptographic binding, any party can modify the payload and re‑encode it, breaking integrity.

To protect against tampering, the token is transformed into a JSON Web Signature (JWS) by appending a third component: the signature. The signature is computed by hashing the Base64URL‑encoded header and payload with the algorithm declared in "alg" and a secret (HMAC) or private key (RSA/ECDSA). The resulting binary signature is then Base64URL‑encoded and concatenated:

(Header).(Payload).(Signature)

Example signed JWT:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZ2UiOjM2fQ.4SkNQ2QZ8z5Lh7W0n2FK8KnXxXq_9yPmyMslK9YpN0A

In this form, any alteration to the header or payload invalidates the signature, allowing the receiver to verify integrity before trusting the claims. This transition from plain Base64URL tokens to signed JWS tokens is the fundamental security step required for authentication and authorization workflows.

JWTs in the OAuth 2.0 and OIDC Landscape

JSON Web Token (JWT) is a JWT format defined by the JOSE specifications. It consists of a Base64URL‑encoded header, payload (claims), and an optional signature. The format itself does not prescribe how a token is obtained or where it is used; that is the responsibility of higher‑level protocols such as OAuth 2.0 and OpenID Connect (OIDC).

OAuth 2.0 is an authorization framework that describes how a resource owner can grant a client limited access to a protected resource. The framework does not mandate a token representation, but in practice most implementations issue access tokens as JWTs because they are self‑contained and can be validated without a network call to the authorization server.

OIDC builds an identity layer on top of OAuth 2.0. It introduces the ID token, which is always a JWT and carries authentication‑related claims (e.g., sub, email, name). The ID token is intended for the client application to establish the user’s identity; it is not meant to be presented to APIs for authorization.

A typical OIDC flow therefore returns three distinct tokens:

  • ID token (JWT) – asserts who the user is; consumed by the client to create a session.
  • Access token (often JWT) – presented to resource servers (APIs) to prove the caller’s permission; APIs must validate its signature and scope.
  • Refresh token (opaque or JWT) – stored securely by the client and used only with the token endpoint to obtain new access (and optionally ID) tokens; never sent to the API.

Example: after a user clicks “Sign in with Google,” the authorization server returns an ID token containing {"sub":"1234567890","email":"user@example.com"}, an access token like eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…, and a refresh token such as 1//0gX... . The client stores the ID token in memory (or a short‑lived cookie) for UI decisions, uses the access token in the Authorization: Bearer header when calling https://api.example.com/userinfo, and exchanges the refresh token when the access token expires.

Key take‑aways for engineers:

  • Never use an ID token as an access token; APIs must validate the access token’s audience and scopes.
  • Refresh tokens should be kept out of browser‑accessible storage (e.g., use httpOnly cookies) to reduce exposure to XSS.
  • When the access token is a JWT, verify its signature, expiration (exp), and audience (aud) on every request.

Why Use JWTs for Authentication?

Signed JSON Web Tokens (JWS) are self‑contained data structures that combine a header, a payload of claims, and a cryptographic signature. The signature—typically generated with HMAC‑SHA‑256 or an RSA/ECDSA algorithm—ensures that only a holder of the secret key (or private key) can create a token that will successfully verify on the server.

Because the payload is trusted after verification, the token can replace traditional session identifiers and, crucially, eliminates the need to store user passwords or session IDs in client‑side storage such as localStorage or cookies. An example of a minimal authentication flow using a signed token is:

// Server side (Node.js)
const jwt = require('jsonwebtoken');
const token = jwt.sign({ sub: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '15m' });
res.json({ accessToken: token });

When the client includes this token in the Authorization: Bearer header, the API can validate the signature and read the claims without any additional lookup.

Key advantages for enterprise applications

  • Eliminates local password storage. The token carries a user identifier (e.g., sub) and any required attributes. Since the signature guarantees integrity, the client never needs to retain the password after the initial login, reducing the attack surface highlighted by OWASP’s authentication cheat sheet.
  • Reduces redundant database queries. Claims such as role, tenant ID, or permission scopes are embedded in the JWT. After signature verification, downstream services can make authorization decisions based on these claims alone, avoiding a round‑trip to the user store for each request. This aligns with NIST SP 800‑63 recommendations for stateless token‑based authentication.
  • Supports horizontal scaling. Because verification requires only the shared secret or public key, any instance of a microservice can authenticate requests without session affinity, simplifying load‑balancer configuration and complying with ISO 27001 requirements for consistent access control.

In practice, an API endpoint might look like:

// Express middleware
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.user = payload; // payload contains id, role, etc.
    next();
  } catch (err) {
    res.sendStatus(401);
  }
}

By trusting the verified payload, the service can authorize the request without querying the user database, thereby improving latency and reducing load on credential stores while maintaining compliance with security standards.

Security Considerations: Storage and Attacks

When a JSON Web Token (JWT) is used for authentication, the location where the token is stored determines the application’s exposure to two of the most common web‑application attacks: cross‑site scripting (XSS) and cross‑site request forgery (CSRF). Understanding the mechanics of each attack helps engineers choose a storage mechanism that aligns with the organization’s security controls (e.g., SOC 2, ISO 27001, NIST 800‑63).

Storage choices and their attack surface

  • LocalStorage / sessionStorage – Tokens are accessible to any JavaScript running in the origin. If an attacker injects malicious script (XSS), they can read the token and replay it against the API. These storages do not send the token automatically with every request, so they are not vulnerable to CSRF, but they are highly susceptible to XSS.
  • HttpOnly, Secure cookies – The browser includes the cookie on every request to the same origin, which eliminates the need for client‑side code to read the token. Because JavaScript cannot access HttpOnly cookies, XSS cannot steal them directly. However, the automatic inclusion makes the application vulnerable to CSRF unless additional mitigations (same‑site attribute, anti‑CSRF tokens) are applied.
  • In‑memory variables – Storing the token only in a JavaScript variable (e.g., after a successful login) limits persistence across page reloads. This reduces the window for XSS theft but does not protect against CSRF if the token is later placed in a cookie.

Mitigating token‑related attacks

Regardless of storage, two token lifecycle controls are essential:

  • Access‑token expiration – Short‑lived access tokens (minutes to hours) limit the usefulness of a stolen token. The server validates the exp claim on each request, rejecting expired tokens.
  • Refresh‑token rotation – When a client presents a valid refresh token, the authorization server issues a new access token **and** a new refresh token, revoking the previous one. If an attacker captures a refresh token, the next legitimate rotation invalidates it, reducing replay risk.

Practical example

// Example of refresh‑token rotation (Node.js/Express)
app.post('/token/refresh', async (req, res) => {
  const oldToken = req.body.refreshToken;
  const payload = await verifyRefreshToken(oldToken); // validates, checks revocation
  const newAccess = signAccessToken(payload.sub);
  const newRefresh = signRefreshToken(payload.sub);
  await revokeToken(oldToken); // ensures rotation
  res.json({ accessToken: newAccess, refreshToken: newRefresh });
});

By pairing HttpOnly, SameSite cookies with short‑lived access tokens and implementing refresh‑token rotation, engineers can mitigate both XSS and CSRF vectors while maintaining compliance with widely accepted security frameworks.

JWTs vs. Server-Side Sessions

JSON Web Tokens (JWTs) are signed, self‑contained strings that convey claims about a user without requiring a database lookup on each request. While this stateless model simplifies scaling, the LogRocket guide lists several practical limitations that affect security and operational control.

Key limitations of JWTs

  • Revocation difficulty: Because a JWT is valid until its expiration, a server cannot invalidate a single token without maintaining a blacklist, which re‑introduces state.
  • Token size: The base64url‑encoded header, payload, and signature increase request size, which can impact bandwidth and header limits.
  • Exposure to XSS: Storing a JWT in localStorage or sessionStorage makes it readable by malicious scripts, increasing the risk of token theft.
  • CSRF considerations: When a JWT is stored in a cookie, it is automatically sent with every request, so an attacker can exploit CSRF unless same‑site or double‑submit cookie patterns are used.
  • No built‑in encryption: JWTs are typically signed but not encrypted; any sensitive data placed in claims is visible to anyone who can read the token.
  • Compliance impact: Standards such as SOC 2, ISO 27001, NIST SP 800‑63, and OWASP ASVS require mechanisms for session termination and audit logging, which are harder to guarantee with purely stateless tokens.

When server‑side sessions are a better fit

  • Immediate revocation is required (e.g., after password change, account lockout, or suspicious activity).
  • Sensitive user data must never travel to the client; storing a session identifier on the server keeps the data protected.
  • The application must comply with strict audit and access‑control requirements that mandate explicit session termination.
  • Low‑latency environments where the overhead of token verification and large headers is undesirable.
  • Legacy APIs that expect a traditional session cookie rather than an Authorization header.

Practical example: In an Express.js service, using express-session with an HttpOnly cookie stores only a random session ID on the client. The server retains the full user profile, can destroy the session instantly, and benefits from built‑in CSRF protection via same‑site cookies. By contrast, a JWT stored in localStorage would require additional client‑side safeguards and a server‑side blacklist to achieve comparable revocation capability.

Editorial Policy & Research Methodology

Our findings are based on rigorous internal research, verified industry benchmarks, and direct technical implementation experience from our enterprise client projects. All statistics and technical claims are reviewed by senior engineers before publication to ensure accuracy, transparency, and helpfulness for our readers.

Have an Idea?

Let's Build Something Amazing Together.