Skip to content
Mar 1

JWT Authentication

MT
Mindli Team

AI-Generated Content

JWT Authentication

JWT authentication is the backbone of stateless security for modern web APIs and single-page applications. Unlike traditional session-based systems that require server-side storage, JWTs enable scalable, decentralized authentication by packaging user identity and permissions into a self-contained token. Understanding how to implement them correctly is critical because a single oversight in token handling can compromise an entire application's security.

Anatomy of a JSON Web Token

A JSON Web Token (JWT) is a compact, URL-safe string that securely transmits information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. A JWT is not encrypted by default; it is encoded and signed. The token structure consists of three distinct parts, separated by dots: the Header, the Payload, and the Signature. The format is xxxxx.yyyyy.zzzzz.

The Header typically consists of two parts: the type of token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA. It is Base64Url encoded to form the first part of the JWT.

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

The Payload contains the claims. Claims are statements about an entity (typically, the user) and additional metadata. There are three types of claims: registered, public, and private claims. Registered claims are predefined (like iss (issuer), exp (expiration time), sub (subject)). Public claims are defined at will, and private claims are custom claims created to share information between parties. This payload is also Base64Url encoded to form the second part of the token.

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022
}

The Signature is the most crucial part for security. To create it, you take the encoded header, the encoded payload, a secret (for HMAC) or a private key (for RSA), and the algorithm specified in the header, and sign them. For example, using the HMAC SHA256 algorithm: HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) The signature ensures the token hasn't been altered. If you change even a single character in the header or payload, the signature will no longer match, and the token will be rejected.

The Authentication Flow: From Login to API Call

The practical flow of JWT authentication is a coordinated dance between the client and server. It begins when a user submits their credentials (e.g., username and password) via a login endpoint. The server validates these credentials against its user database. If they are correct, the server's application logic creates the JWT. It builds the header and payload with the necessary claims (like user_id, role, exp) and then signs it using a secret key or private key that never leaves the server.

The server then sends this signed JWT back to the client, typically in the body of the HTTP response. The client application (e.g., a React app or mobile app) is responsible for secure storage of this token. The most common practice is to store it in memory or in an HttpOnly cookie to mitigate XSS (Cross-Site Scripting) risks; local storage is generally discouraged for sensitive tokens due to its vulnerability to XSS attacks.

For every subsequent request to a protected API endpoint, the client must include the JWT in the request headers. This is almost always done using the Authorization header with the Bearer scheme: Authorization: Bearer <your.jwt.token> When the server receives this request, it extracts the token from the header, verifies the signature using its secret/public key, and validates the standard claims (like exp and iss). If all checks pass, the server trusts the claims inside the payload and grants access to the requested resource. This process is stateless because the server does not need to keep a record of which tokens are issued; it simply validates each token presented to it.

Managing Token Lifespan: Expiration and Refresh Tokens

A bare JWT with a fixed expiration (set via the exp claim) presents a security dilemma: make it too long-lived, and a stolen token is valid for a dangerous period; make it too short-lived, and the user experience is ruined by frequent logouts. The standard solution is to use a two-token system: a short-lived access token (the JWT) and a long-lived refresh token.

The access token, with an expiration of 15-60 minutes, is used for API access as described above. The refresh token, which is stored securely on the server (e.g., in a database) and issued alongside the access token, has a much longer lifespan (days, weeks). When the access token expires, the client can send the refresh token to a dedicated /refresh endpoint. The server validates this refresh token against its stored list, and if valid, issues a new access token. This keeps the user logged in seamlessly.

The refresh token must be revoked upon user logout, which requires a stateful operation on the server (deleting the stored refresh token). This architecture provides a balance between stateless API access and secure session management. A more secure pattern is refresh token rotation, where a new refresh token is issued with each new access token, and the old one is invalidated. This helps detect token theft because an attacker trying to use a stolen refresh token will invalidate the legitimate user's token.

Common Pitfalls

1. Storing JWTs Insecurely on the Client.

  • Mistake: Storing the JWT in localStorage or sessionStorage. This makes it accessible to JavaScript, and therefore vulnerable to theft via XSS attacks.
  • Correction: For single-page applications, store the token in memory (JavaScript variable) or, preferably, in an HttpOnly, SameSite=Strict cookie. The HttpOnly flag prevents JavaScript access, mitigating XSS. The SameSite flag helps protect against CSRF attacks.

2. Failing to Verify the Signature or Validate Claims.

  • Mistake: Simply decoding the Base64Url payload and trusting its contents without verifying the signature. Attackers can easily forge tokens if the signature isn't checked.
  • Correction: Always use your authentication library's verification function. This function should check the signature with your secret/key and validate standard claims like exp (expiration) and iat (issued at). Never manually "decode and trust."

3. Putting Sensitive Data in the Payload.

  • Mistake: Including passwords, social security numbers, or other secrets in the JWT payload. Remember, the payload is only Base64 encoded, not encrypted. Anyone who intercepts the token can decode and read it.
  • Correction: The payload should only contain non-sensitive claims necessary for authorization (like user_id, role, scope). Treat the JWT contents as you would treat data you'd write on a postcard.

4. Using Weak Signing Secrets or Leaking Keys.

  • Mistake: Using a simple, guessable string like "secret123" as the HMAC secret or accidentally committing your private key to a public GitHub repository.
  • Correction: Use strong, cryptographically random secrets. Store secrets and private keys in environment variables or dedicated secret management services (e.g., AWS Secrets Manager, HashiCorp Vault). Never hardcode them.

Summary

  • A JWT is a signed token composed of a Header (algorithm), Payload (claims), and Signature, enabling stateless authentication.
  • The standard flow involves the server issuing a JWT after login, the client storing it securely (preferably in an HttpOnly cookie), and sending it in the Authorization: Bearer header for subsequent API requests.
  • Token expiration is managed by pairing a short-lived access token (JWT) with a long-lived, server-side refresh token to balance security and user experience.
  • Critical security practices include always verifying the token signature, never storing sensitive data in the payload, and using strong, properly stored secrets to prevent authentication bypass.

Write better notes with AI

Mindli helps you capture, organize, and master any subject with AI-powered summaries and flashcards.