What's JWT

Views

JWT, or JSON Web Token, is a compact and self-contained method for securely transmitting information between two parties as a JSON object. It's commonly used in authentication and authorization mechanisms in web applications. JWTs are used to verify the identity of users and authorize them to access certain resources without requiring their credentials to be stored or transmitted repeatedly.

Key components of a JWT

A JWT consists of three key components header, payload and signature.

Contains metadata about the token, such as the type of token (JWT) and the signing algorithm being used (e.g., HS256 or RS256).

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

Payload

Contains the claims, which are the data you want to transmit. This can include user information or any other data the token needs to carry.

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

Common claims include:

  • sub: Subject (user identifier)

  • iat: Issued at time (timestamp when the token was created)

  • exp: Expiration time (timestamp when the token expires)

  • Custom claims like user name, role, permissions, etc.

Signature

The signature is used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn't altered along the way. The signature is created by encoding the header and payload, and then signing that string using the secret or private key and the algorithm specified in the header.

JWT in practice: Generating and Verifying Tokens with HMAC (SHA-256)

In this example, I'll show how to generate and verify a JWT using HMAC with SHA-256, a symmetric signing algorithm. We'll use a shared secret for both signing and verifying the token.

generate-token.mjs
import crypto from 'crypto'
 
// Function to create a base64url-encoded string
const base64UrlEncode = (data) => {
  return Buffer.from(JSON.stringify(data)).toString('base64url') // Using base64url for JWT
}
 
// Function to create a JWT
const createJWT = (payload) => {
  // Create the header
  const header = {
    alg: 'HS256',
    typ: 'JWT',
  }
 
  // Base64Url encode the header and payload
  const encodedHeader = base64UrlEncode(header)
  const encodedPayload = base64UrlEncode(payload)
 
  // Create the signature
  const signatureBase = `${encodedHeader}.${encodedPayload}`
  const signature = crypto
    .createHmac('sha256', process.env.SECRET_KEY)
    .update(signatureBase)
    .digest('base64url')
 
  // Combine the parts to create the JWT
  const jwt = `${encodedHeader}.${encodedPayload}.${signature}`
  return jwt
}
 
export default createJWT

The function createJWT takes a payload (user data) and signs it using the SECRET_KEY with HMAC SHA-256. We base64url-encode both the header and payload, then append the signature to complete the token.

verify-token.mjs
import crypto from 'crypto'
 
const base64UrlDecode = (str) => {
  str += '='.repeat((4 - (str.length % 4)) % 4) // Add padding for Base64 decoding
  return Buffer.from(str, 'base64').toString() // Decode Base64
}
 
const verifyJWT = (token) => {
  const parts = token.split('.')
  if (parts.length !== 3) {
    throw new Error('Token structure is invalid') // Ensure the token has three parts
  }
 
  const [encodedHeader, encodedPayload, signature] = parts
 
  // Create the signature base
  const signatureBase = `${encodedHeader}.${encodedPayload}`
 
  // Recreate the signature
  const recreatedSignature = crypto
    .createHmac('sha256', process.env.SECRET_KEY)
    .update(signatureBase)
    .digest('base64url')
 
  // Check if the recreated signature matches the signature in the token
  if (recreatedSignature !== signature) {
    throw new Error('Invalid signature') // Signature mismatch
  }
 
  // Decode and return the payload
  const decodedPayload = JSON.parse(base64UrlDecode(encodedPayload))
 
  // Check expiration if present
  if (
    decodedPayload.exp &&
    Math.floor(Date.now() / 1000) >= decodedPayload.exp
  ) {
    throw new Error('Token has expired') // Expired token
  }
 
  return decodedPayload // Return the decoded payload if verification is successful
}
 
export default verifyJWT

When verifying the token, we decode the signature, check its integrity, and verify the expiration (exp) claim if present.

app.mjs
import createJWT from './generate-token.mjs'
import verifyJWT from './verify-token.mjs'
 
// Define the payload data
const payload = {
  id: 1,
  username: 'vinicius_cestari',
  role: 'admin',
  exp: Math.floor(Date.now() / 1000) + 60 * 60, // Expires in 1 hour
}
 
// Create the JWT
const token = createJWT(payload)
 
// Output the token
console.log('Generated JWT:', token) // Generated JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ2aW5pY2l1c19jZXN0YXJpIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzI4NjU0NjU5fQ.YdPCLiejG39jzG3sT8Lcx1NpZ-qNzhKL9kjfwXqybow
 
console.log('Decoded JWT:', verifyJWT(token)) // Decoded JWT: { id: 1, username: 'vinicius_cestari', role: 'admin', exp: 1728655348 }

How JWT works

The server generates a token (usually after successful login), signs it with a secret or private key, and sends it to the client.

The client typically stores the token in localStorage (which is generally not recommended due to potential security risks) or cookies.The token is then sent with each request to the server, allowing the server to verify its authenticity using the signing key. This process ensures that only valid tokens can access protected resources.

Key Use Cases

  • Authentication: After a user logs in, a JWT is issued. The client sends this token with each subsequent request, allowing the server to verify the user's identity without needing to log in again.

  • Authorization: JWTs can also carry information about what resources the user can access, allowing servers to restrict access based on user roles or permissions.

Conclusion

JWTs offer a robust and stateless way to handle authentication and authorization. By securely transmitting information via a compact token, JWTs enable web applications to verify user identity and permissions efficiently. The tokens are self-contained, meaning they can carry all the necessary information without requiring repetitive access to the server for session data. This results in a more scalable and performant system.

For practical implementation, it’s essential to:

  1. Keep your secret key secure: In symmetric algorithms like HMAC, this key is what ensures the integrity of the token. If compromised, unauthorized users could generate valid tokens.

  2. Use short expiration times: With sensitive data, short-lived tokens minimize risk if a token is stolen. When a token expires, clients can request a new one if they’re still authenticated.

  3. Store tokens safely: Store JWTs securely, preferably in httpOnly cookies to prevent JavaScript access and reduce the risk of cross-site scripting (XSS) attacks.

JWTs are widely supported and compatible with many libraries and frameworks, making them a versatile choice for securing modern web applications.