JWT, or JSON Web Token, is a compact, self-contained way to represent information between two parties, typically used for authentication and authorization in web applications. It is an open standard (RFC 7519) that uses JSON objects to transmit data, which is digitally signed to ensure integrity and authenticity.

A JWT consists of three main parts:

  1. Header: Metadata about the token, such as the signing algorithm (e.g., HMAC SHA256 or RSA).
  2. Payload: The actual data (claims) being transmitted, such as user ID, roles, or expiration time.
  3. Signature: A cryptographic signature to verify the token’s integrity.

These parts are Base64Url-encoded and separated by dots (.), forming a token like this:
header.payload.signature

For example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Key Knowledge Points of JWT

  1. Structure:
    • Header: Typically contains:
      • alg: The algorithm used for signing (e.g., HS256 for HMAC-SHA256, RS256 for RSA).
      • typ: The token type, usually JWT.
      • Example: {"alg": "HS256", "typ": "JWT"}.
    • Payload: Contains claims, which are statements about an entity (e.g., user) and additional data. Claims are categorized as:
      • Registered Claims: Predefined claims like iss (issuer), sub (subject), aud (audience), exp (expiration), iat (issued at), nbf (not before).
      • Public Claims: Custom claims defined by the application (e.g., user_id, roles).
      • Private Claims: Application-specific claims shared between parties.
      • Example: {"sub": "1234567890", "name": "John Doe", "iat": 1516239022}.
    • Signature: Created by taking the encoded header, encoded payload, and a secret (or private key for asymmetric algorithms), then signing with the specified algorithm. For HS256:
      HMAC-SHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret).
  2. Security:
    • Integrity: The signature ensures the token hasn’t been tampered with.
    • Confidentiality: JWTs are not encrypted by default; the payload is only Base64-encoded, so sensitive data should not be stored in the payload unless encrypted (e.g., using JWE - JSON Web Encryption).
    • Signing Algorithms:
      • Symmetric: Uses a single secret key (e.g., HS256). Suitable for trusted parties sharing the secret.
      • Asymmetric: Uses a private-public key pair (e.g., RS256). The private key signs the token, and the public key verifies it, ideal for distributed systems.
    • Expiration: Always include an exp claim to limit token validity and mitigate replay attacks.
    • Key Management: Store secrets securely (e.g., environment variables, secret management systems like AWS Secrets Manager).
    • Avoiding Vulnerabilities:
      • Prevent algorithm confusion attacks by explicitly validating the alg header.
      • Use strong secrets or key pairs.
      • Avoid storing sensitive data in the payload.
  3. Use Cases:
    • Authentication: After a user logs in, the server generates a JWT containing user information and sends it to the client. The client includes the JWT in subsequent requests (usually in the Authorization header as Bearer <token>).
    • Authorization: The server verifies the JWT to grant access to protected resources based on claims (e.g., roles or permissions).
    • Single Sign-On (SSO): JWTs can be used across multiple services if the same signing key or public key is shared.
    • Statelessness: JWTs are self-contained, so the server doesn’t need to store session data, making them ideal for scalable APIs.
  4. Advantages:
    • Stateless: No server-side session storage required.
    • Scalable: Works well in microservices or distributed systems.
    • Cross-Domain: Can be used across different domains (e.g., for SSO).
    • Compact: Base64 encoding makes it lightweight.
  5. Disadvantages:
    • Non-Revocable: Once issued, a JWT is valid until it expires unless a revocation mechanism (e.g., a blacklist) is implemented.
    • Payload Size: Large payloads increase token size, impacting performance.
    • Security Risks: Misconfiguration (e.g., weak secrets, missing exp) can lead to vulnerabilities.

Using JWT in TypeScript and Go

Below, I’ll explain how to implement JWT-based authentication in a web application using TypeScript (for the frontend or Node.js backend) and Go (for the backend). I’ll assume a REST API scenario where the client authenticates with a username/password, receives a JWT, and uses it for subsequent requests.

1. Go Backend (Golang)

We’ll use the github.com/golang-jwt/jwt/v5 package for creating and verifying JWTs in Go.

Installation
go get github.com/golang-jwt/jwt/v5
Example: JWT Authentication API

Here’s a Go backend that:

  • Handles user login and generates a JWT.
  • Protects endpoints with JWT verification.
package main

import (
	"encoding/json"
	"fmt"
	"github.com/golang-jwt/jwt/v5"
	"net/http"
	"time"
)

// Simulated user for demo purposes
type User struct {
	ID       string `json:"id"`
	Username string `json:"username"`
	Password string `json:"password"` // In production, use hashed passwords
}

type LoginRequest struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

type Claims struct {
	UserID   string `json:"user_id"`
	Username string `json:"username"`
	jwt.RegisteredClaims
}

// Secret key for signing (store in environment variables in production)
var jwtSecret = []byte("your-secure-secret")

func main() {
	http.HandleFunc("/login", loginHandler)
	http.HandleFunc("/protected", jwtMiddleware(protectedHandler))
	http.ListenAndServe(":8080", nil)
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	var req LoginRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request", http.StatusBadRequest)
		return
	}

	// Simulate user validation (replace with database query)
	user := User{ID: "1", Username: "john", Password: "password"}
	if req.Username != user.Username || req.Password != user.Password {
		http.Error(w, "Invalid credentials", http.StatusUnauthorized)
		return
	}

	// Create JWT
	claims := &Claims{
		UserID:   user.ID,
		Username: user.Username,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			Subject:   user.ID,
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString(jwtSecret)
	if err != nil {
		http.Error(w, "Failed to generate token", http.StatusInternalServerError)
		return
	}

	// Return JWT to client
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"token": tokenString})
}

func jwtMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Extract token from Authorization header
		authHeader := r.Header.Get("Authorization")
		if authHeader == "" || len(authHeader) < 7 || authHeader[:7] != "Bearer " {
			http.Error(w, "Missing or invalid token", http.StatusUnauthorized)
			return
		}

		tokenString := authHeader[7:] // Remove "Bearer " prefix
		claims := &Claims{}

		// Parse and verify token
		token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
				return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
			}
			return jwtSecret, nil
		})

		if err != nil || !token.Valid {
			http.Error(w, "Invalid token", http.StatusUnauthorized)
			return
		}

		// Token is valid, call the next handler
		next(w, r)
	}
}

func protectedHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("This is a protected endpoint!"))
}

Key Points:

  • Claims: We define a custom Claims struct embedding jwt.RegisteredClaims for standard claims like exp and iat.
  • Signing: We use HS256 with a secret key. For production, store the secret securely.
  • Middleware: The jwtMiddleware verifies the token and ensures only valid tokens access protected routes.
  • Security: Always validate the signing method to prevent algorithm confusion attacks.
  • Expiration: Set an ExpiresAt to limit token validity (e.g., 24 hours).
Production Considerations
  • Use a secure secret or RSA keys for signing.
  • Store user credentials in a database with hashed passwords (e.g., bcrypt).
  • Implement refresh tokens for long-lived sessions.
  • Consider token revocation (e.g., maintain a blacklist in Redis).
  • Use HTTPS to prevent token interception.

2. TypeScript (Frontend or Node.js Backend)

For TypeScript, we’ll use the jsonwebtoken package for Node.js (if building a backend) or handle JWTs on the frontend by sending requests with the token.

Installation

If using Node.js:

npm install jsonwebtoken
npm install @types/jsonwebtoken --save-dev

If building a frontend, you’ll typically use fetch or a library like axios to send requests with the JWT.

Example 1: Node.js Backend with TypeScript

Here’s a simple Node.js Express backend that generates and verifies JWTs.

import express from 'express';
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';

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

// Secret key (store in environment variables in production)
const JWT_SECRET = 'your-secure-secret';

// Simulated user
interface User {
  id: string;
  username: string;
  password: string;
}

const user: User = { id: '1', username: 'john', password: 'password' };

interface JwtPayload {
  user_id: string;
  username: string;
}

// Login endpoint
app.post('/login', (req: Request, res: Response) => {
  const { username, password } = req.body;

  if (username !== user.username || password !== user.password) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const payload: JwtPayload = {
    user_id: user.id,
    username: user.username,
  };

  const token = jwt.sign(payload, JWT_SECRET, {
    expiresIn: '24h', // Token expires in 24 hours
  });

  res.json({ token });
});

// JWT middleware
const authenticateJWT = (req: Request, res: Response, next: NextFunction) => {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or invalid token' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const payload = jwt.verify(token, JWT_SECRET) as JwtPayload;
    (req as any).user = payload; // Attach user to request
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

// Protected endpoint
app.get('/protected', authenticateJWT, (req: Request, res: Response) => {
  res.json({ message: 'This is a protected endpoint!', user: (req as any).user });
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Key Points:

  • Signing: The jsonwebtoken library signs the token with the secret and an expiration time.
  • Middleware: The authenticateJWT middleware verifies the token and attaches the payload to the request.
  • Type Safety: We define interfaces for the payload to ensure type safety.
Example 2: TypeScript Frontend (React)

Here’s how to use JWT in a React frontend with axios to communicate with the Go backend.

import axios from 'axios';
import { useState } from 'react';

const API_URL = 'http://localhost:8080';

function App() {
  const [token, setToken] = useState<string | null>(null);
  const [message, setMessage] = useState<string>('');

  // Login function
  const login = async () => {
    try {
      const response = await axios.post(`${API_URL}/login`, {
        username: 'john',
        password: 'password',
      });
      setToken(response.data.token);
      localStorage.setItem('token', response.data.token); // Store token
    } catch (err) {
      console.error('Login failed', err);
    }
  };

  // Access protected endpoint
  const accessProtected = async () => {
    try {
      const response = await axios.get(`${API_URL}/protected`, {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });
      setMessage(response.data);
    } catch (err) {
      console.error('Failed to access protected endpoint', err);
    }
  };

  return (
    <div>
      <button onClick={login}>Login</button>
      <button onClick={accessProtected} disabled={!token}>
        Access Protected Endpoint
      </button>
      <p>{message}</p>
    </div>
  );
}

export default App;

Key Points:

  • Token Storage: Store the JWT in localStorage or sessionStorage (consider security implications; HTTP-only cookies are safer).
  • Authorization Header: Include the JWT in the Authorization header as Bearer <token>.
  • Error Handling: Handle expired or invalid tokens gracefully.
Production Considerations
  • Secure Storage: Avoid storing JWTs in localStorage due to XSS risks. Use HTTP-only, secure cookies instead.
  • Refresh Tokens: Implement refresh tokens to issue new JWTs without re-authentication.
  • CORS: Ensure the backend supports CORS if the frontend is on a different domain.
  • HTTPS: Always use HTTPS to prevent token interception.

Best Practices for JWT in TypeScript and Go

  1. Security:
    • Use strong, unique secrets or key pairs.
    • Always include exp and validate it.
    • Use HTTPS to encrypt communication.
    • Avoid storing sensitive data in the payload unless encrypted.
  2. Token Management:
    • Implement refresh tokens for long-lived sessions.
    • Consider token revocation (e.g., blacklisting in a database or cache).
    • Use short-lived access tokens (e.g., 15 minutes) with refresh tokens.
  3. Performance:
    • Keep payloads small to reduce token size.
    • Cache public keys in asymmetric signing for faster verification.
  4. Error Handling:
    • Handle token expiration and invalid tokens gracefully.
    • Provide clear error messages to clients (without exposing sensitive details).
  5. Testing:
    • Test token generation and verification thoroughly.
    • Simulate edge cases like expired tokens, tampered tokens, or missing headers.
  6. Libraries:
    • Go: Use github.com/golang-jwt/jwt/v5 for robust JWT handling.
    • TypeScript/Node.js: Use jsonwebtoken for signing/verification.
    • Frontend: Use axios or fetch for HTTP requests.

Example Workflow

  1. User Login:
    • Client sends username/password to /login.
    • Server validates credentials, generates a JWT with user info, and returns it.
    • Client stores the JWT (e.g., in localStorage or a cookie).
  2. Accessing Protected Resources:
    • Client includes the JWT in the Authorization header (Bearer <token>).
    • Server verifies the token’s signature, expiration, and claims.
    • If valid, the server processes the request; otherwise, it returns a 401 error.
  3. Token Refresh (Optional):
    • When the access token expires, the client uses a refresh token to request a new access token.
    • Server validates the refresh token and issues a new JWT.

Common Pitfalls and How to Avoid Them

  1. Weak Secrets:
    • Use cryptographically secure secrets or RSA keys.
    • Store secrets in environment variables or secret management systems.
  2. No Expiration:
    • Always include an exp claim and validate it.
  3. Storing Sensitive Data:
    • Avoid putting sensitive information in the payload unless encrypted (e.g., with JWE).
  4. Algorithm Confusion:
    • Explicitly validate the alg header to prevent attacks where attackers manipulate the algorithm.
  5. Token Revocation:
    • Implement a revocation mechanism (e.g., blacklisting tokens in Redis) for logout or compromised tokens.

Conclusion

JWT is a powerful tool for authentication and authorization in web applications, particularly in stateless, scalable APIs. In your TypeScript and Go stack:

  • Use github.com/golang-jwt/jwt/v5 in Go for backend JWT handling.
  • Use jsonwebtoken in TypeScript/Node.js for backend or axios/fetch in the frontend.
  • Follow security best practices like using strong secrets, enforcing expiration, and using HTTPS.
  • Consider refresh tokens and revocation for production systems.

Comments