Stackvora.tech

JWT Basics & Best Practices

JSON Web Tokens (JWTs) provide a compact, URL-safe means of representing claims between two parties. They are widely adopted in modern web architectures…

JSON Web Tokens (JWTs) provide a compact, URL-safe means of representing claims between two parties. They are widely adopted in modern web architectures for stateless authentication and authorization, enabling microservices and single-page applications to operate efficiently without direct session management overhead. Understanding their structure, use cases, and security implications is critical for secure system design.

JWT Structure and Components

A JWT consists of three parts separated by dots, following the structure header.payload.signature. Each component is Base64url-encoded, meaning it can be easily transmitted in environments like HTTP headers or URL query parameters.

1. Header (JOSE Header)

The header typically contains two fields:

  • alg: The algorithm used for signing the token. Common values include HS256 (HMAC using SHA-256) and RS256 (RSA using SHA-256).
  • typ: The type of token, which is usually JWT.

Example decoded header:

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

2. Payload (JWT Claims)

The payload contains the "claims" about an entity (e.g., a user) and additional data. Claims are key-value pairs. There are three types of claims:

  • Registered Claims: Standardized claims, not mandatory but recommended to provide a set of useful, interoperable claims. Examples include:
    • iss (Issuer): Identifies the principal that issued the JWT.
    • sub (Subject): Identifies the principal that is the subject of the JWT.
    • aud (Audience): Identifies the recipients that the JWT is intended for.
    • exp (Expiration Time): The time after which the JWT MUST NOT be accepted for processing. Measured in seconds since epoch.
    • nbf (Not Before): The time before which the JWT MUST NOT be accepted for processing.
    • iat (Issued At): The time at which the JWT was issued.
    • jti (JWT ID): A unique identifier for the JWT.
  • Public Claims: Custom claims defined by implementers. To avoid collisions, they should be defined in the IANA JSON Web Token Claims Registry or be defined as a URI that contains a collision-resistant namespace.
  • Private Claims: Custom claims created to share information between parties that agree on their use. They are not registered or public and are subject to collision.

Example decoded payload:

{
  "sub": "user123",
  "name": "Jane Doe",
  "roles": ["admin", "editor"],
  "iat": 1678886400,
  "exp": 1678886700,
  "iss": "https://api.example.com",
  "aud": "https://dashboard.example.com"
}

Crucial point: The payload is Base64url-encoded, not encrypted. Anyone can decode it. Therefore, never put sensitive, confidential information (e.g., credit card numbers, passwords) directly into the JWT payload.

3. Signature

The signature is used to verify that the sender of the JWT is who it claims to be and that the message hasn't been altered along the way. It is created by taking the Base64url-encoded header, the Base64url-encoded payload, a secret (for symmetric algorithms like HS256) or a private key (for asymmetric algorithms like RS256), and running it through the algorithm specified in the header.

Signature calculation formula (simplified):

HMACSHA256(
  base64urlEncode(header) + "." +
  base64urlEncode(payload),
  secret
)

A typical JWT will look like this (abbreviated):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT Usage Flow

  1. Authentication: User provides credentials (username/password) to an authentication server.
  2. Token Issuance: Upon successful authentication, the server generates a JWT containing user claims and signs it.
  3. Token Transmission: The server sends the JWT back to the client. This is often done as an HTTP-only, secure cookie or in the Authorization header as a Bearer token (e.g., Authorization: Bearer <token>).
  4. Resource Access: For subsequent requests, the client sends the JWT to the resource server.
  5. Token Verification: The resource server verifies the JWT's signature using the shared secret or public key. It also validates claims like exp, iss, and aud.
  6. Authorization: If the token is valid, the server uses the claims (e.g., roles) to determine if the client is authorized to access the requested resource.

Best Practices for Secure JWT Implementation

1. Signature Algorithm Selection

Choose robust signing algorithms:

  • HS256 (HMAC with SHA-256): Suitable when both the issuer and the verifier share a secret key. Simpler to implement but requires secure management of the shared secret across all services that need to verify tokens. Key length should be at least 256 bits (32 bytes).
  • RS256 (RSA with SHA-256): Recommended for distributed systems where multiple resource servers need to verify tokens issued by a single authentication server. The authentication server signs with its private key, and resource servers verify with its public key. This avoids distributing the private key.

Avoid "None" algorithm: Some JWT libraries support "alg": "none", which means no signature verification is performed. This is a severe security vulnerability if not handled correctly and should be explicitly disabled in your JWT library or rejected by your verification logic.

2. Token Lifetime Management

Balancing security and user experience is key:

  • Short-lived Access Tokens: Set a short expiration time (e.g., 5-15 minutes) for access tokens (exp claim). This minimizes the window of opportunity for an attacker if a token is compromised.
  • Long-lived Refresh Tokens: Use refresh tokens to obtain new access tokens without re-authenticating the user. Refresh tokens should be long-lived (e.g., days, weeks, or months) and securely stored. They should ideally be one-time use and rotated, meaning each refresh request invalidates the old refresh token and issues a new one.
  • Revocation: Access tokens cannot be "revoked" once issued (they are stateless). Revocation must be handled at a higher level, typically by revoking the refresh token or by maintaining a blocklist/blacklist of compromised access tokens on the resource server (which adds state).

3. Secure Token Storage

Where and how clients store JWTs is critical:

  • HttpOnly, Secure Cookies (Recommended for SPAs/Web Apps): Store JWTs in cookies with the HttpOnly flag. This prevents client-side JavaScript from accessing the cookie, mitigating XSS attacks. The Secure flag ensures the cookie is only sent over HTTPS. Prefix cookie names with __Host- or __Secure- for additional security (e.g., __Host-jwt_access_token).
  • Browser Local Storage/Session Storage (Use with Caution): While seemingly convenient, storing JWTs here makes them vulnerable to XSS attacks, as malicious JavaScript can easily access them. If used, ensure robust content security policies (CSPs) and careful input sanitization are in place.

4. Comprehensive Claim Validation

Always validate essential claims on the receiving end:

  • exp (Expiration Time): Crucial. Reject tokens that have expired.
  • iss (Issuer): Verify that the token was issued by an expected authority. E.g., "https://auth.example.com".
  • aud (Audience): Ensure the token is intended for your service. E.g., if your API is https://api.example.com, the audience claim should match.
  • nbf (Not Before): If present, ensure the current time is after the "not before" time.
  • Signature Verification: Always verify the token's signature using the correct secret or public key.

Example validation pseudo-code:

// Using a Go-like pseudo-code
func ValidateJWT(tokenString string, secret string) (claims map[string]interface{}, err error) {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        // Validate signing algorithm
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return []byte(secret), nil // Use your shared secret
    })

    if err != nil {
        return nil, err
    }

    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        // Validate 'exp' claim (handled by library typically)
        // Manual validation for 'iss'
        if claims["iss"] != "https://auth.example.com" {
            return nil, fmt.Errorf("invalid issuer")
        }
        // Manual validation for 'aud'
        if claims["aud"] != "https://api.example.com" {
            return nil, fmt.Errorf("invalid audience")
        }
        return claims, nil
    }
    return nil, fmt.Errorf("invalid token")
}

Most robust JWT libraries (e.g., golang-jwt/jwt for Go, jsonwebtoken for Node.js, PyJWT for Python) handle exp, nbf, iat, iss, and aud validation automatically when configured correctly.

5. HTTPS Everywhere

Always transmit JWTs over HTTPS (TLS/SSL). This encrypts the token in transit, preventing eavesdropping and man-in-the-middle attacks that could steal the token.

6. Don't Put Secrets in the Payload

Reiterate: The payload is Base64url-encoded, not encrypted. It's easily decoded. Assume any information in the payload is publicly visible. Only include non-sensitive claims necessary for authorization decisions (e.g., user ID, roles, permissions).

Common Pitfalls

  • Mismanaging Secrets: Using weak secrets, hardcoding secrets, or allowing them to be publicly exposed. For HS256, use a cryptographically strong, sufficiently long secret (e.g., 256 bits/32 bytes, generated with a secure random number generator).
  • Ignoring Expiration (exp): Failing to validate the exp claim leaves your system open to replay attacks with stale tokens.
  • Lack of Audience (aud) Validation: A token issued for one service might be mistakenly or maliciously accepted by another if aud isn't validated.
  • XSS Vulnerability: Storing JWTs in localStorage without proper XSS protection allows attackers to steal tokens.
  • Algorithm Confusion Attacks: Exploits where an attacker changes the alg header from RS256 to HS256 and signs it with a public key, which the server then incorrectly interprets as a shared secret. Robust JWT libraries typically mitigate this by verifying the expected algorithm type and key usage.
  • Stateless Fallacy: While JWTs are stateless for the resource server, session management (revocation, logout) still requires some form of state, usually managed through refresh tokens or a blocklist/blacklist, which can add complexity.

Back to the knowledge base · Ask the AI assistant