In the world of modern web development, JSON Web Tokens (JWT) have become the gold standard for handling authentication and secure data exchange. But what exactly happens when a server issues a token? Is it encrypted? Can anyone read it?
This guide breaks down the mechanics of JWTs, clarifies the common confusion between “encoding” and “encryption,” and shows you how to implement them in Go (Golang).
The Mechanics: How JWT Works
A JWT is a compact, URL-safe means of representing claims to be transferred between two parties. Unlike traditional session-based authentication where the server keeps a record of logged-in users (stateful), JWTs are stateless. The token itself contains all the necessary information to verify the user.
1. The Anatomy of a Token
If you look at a JWT, it appears as a long string of random characters separated by two dots. It follows this structure:
Header.Payload.Signature
-
Header: Indicates the algorithm used (e.g.,
HS256) and the token type (JWT). - Payload: Contains the “Claims” (user data like ID, role, expiration time).
- Signature: The security seal.
2. Secrecy vs. Integrity
A common question developers ask is: “If the token is just Base64 encoded, can’t anyone read the signature and the payload?”
The answer is yes. Anyone who intercepts the token can decode and read the payload. However, the security of a JWT doesn’t come from hiding the data; it comes from ensuring the data hasn’t been changed.
The “Wax Seal” Analogy
Think of a JWT like a letter sent by a King (the Server) to a Messenger (the Client).
- The Payload: The content of the letter (“Give this messenger 10 gold coins”).
- The Signature: The King’s wax seal stamped at the bottom.
Anyone can see the wax seal. It isn’t hidden. But no one can copy or forge the seal because they don’t have the King’s signet ring (the Secret Key ).
If a thief changes the letter to say “Give this messenger 10,000 gold coins,” the wax seal will no longer match the content. They cannot “re-seal” the letter because they lack the King’s ring.
The Technical Reality: Encoding vs. Encryption
The signature is not plain text , nor is it encrypted. It is a binary cryptographic hash that is Base64Url encoded.
The Math: The server takes the Header and Payload and runs them through a hashing algorithm (like HMAC-SHA256) using a Secret Key.
The Encoding: The output of this hash is raw binary data (bytes). To make it safe for URLs and HTTP headers, we Base64Url Encode these bytes into text characters (
A-Z,0-9,-,_).
This ensures that even though anyone can read the token, no one can generate a valid signature for a fake payload without your server’s secret key.
Implementing JWT in Golang
Let’s look at how to implement this using the industry-standard package github.com/golang-jwt/jwt/v5.
Step 1: Define Your Claims
First, we define what data we want to store inside the token.
import (
"github.com/golang-jwt/jwt/v5"
"time"
)
// Define a custom struct to hold the token claims
type UserClaims struct {
UserID string `json:"user_id"`
Role string `json:"role"`
jwt.RegisteredClaims // Embed standard claims (exp, iss, etc.)
}
// In production, keep this safe! Never hardcode it in your source code.
var jwtKey = []byte("my_secret_key")
Step 2: Generating a Token (Login)
When a user logs in successfully, we generate the token. Notice how we “sign” it at the end—this is where the hashing happens.
func GenerateToken(userID, role string) (string, error) {
// 1. Create the Claims
claims := UserClaims{
UserID: userID,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)), // Short expiry!
Issuer: "my-app",
},
}
// 2. Create the token using the HS256 algorithm
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 3. Sign the token with our secret key
// This generates the binary hash and Base64 encodes it for you
signedToken, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return signedToken, nil
}
Step 3: Validating the Token (Middleware)
When the server receives a token, it doesn’t “decrypt” it. Instead, it re-calculates the hash using the jwtKey to see if it matches the signature provided.
func VerifyToken(tokenString string) (*UserClaims, error) {
// Parse the token
token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
// Validate the alg is what you expect (Crucial security step!)
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtKey, nil
})
if err != nil {
return nil, err
}
// Check if the token is valid (signature match) and not expired
if claims, ok := token.Claims.(*UserClaims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
When Should You Use JWT?
1. Authorization & Single Sign-On (SSO)
This is the most common use case. Because JWTs are self-contained, they are easily passed between different domains (e.g., auth.example.com and app.example.com), making them perfect for SSO.
2. Secure Information Exchange
Because the tokens are signed, the receiver can be 100% sure that the sender is who they claim to be. If the signature matches, the data is authentic.
Pros and Cons of JWT
The Pros
- Stateless Scalability: The server doesn’t need to store session info, making it trivial to scale across multiple servers.
- Mobile Friendly: Native mobile apps handle JSON and headers much easier than cookies.
- Performance: No database lookup is required to verify the token (only to check revocation lists if you use them).
The Cons & Security Considerations
- Payload Visibility: Never put passwords or sensitive personal data (PII) in the payload. Remember, anyone can base64 decode it and read it.
-
Revocation Difficulty: You cannot “log out” a user instantly by deleting a server session.
- Solution: Use Short-Lived Access Tokens (e.g., 15 mins) paired with Refresh Tokens.
-
Storage Matters:
- LocalStorage: Vulnerable to XSS (Cross-Site Scripting).
- HttpOnly Cookies: Vulnerable to CSRF, but safer against XSS. Recommended for web apps.
Summary
JWTs offer a modern, lightweight approach to authentication. The key takeaway is that they guarantee integrity , not secrecy. The signature ensures that the data you receive is exactly what was sent, signed by a trusted party. By understanding this distinction, you can build secure, scalable Go applications without falling into common security traps.
Top comments (0)