Key Differences:
– Basic Auth
– JWT
1. Credentials sent
– Username/password sent with every request (Base64 encoded)
– Credentials sent once; subsequent requests use token
2. Server validation
– Database query needed for every request
– Can validate cryptographically without database lookup
3. Stateless
– Stateful (server validates against stored credentials)
– Stateless (verification is self-contained)
4. Token expiration
– Credentials always valid until password changes
– Tokens expire automatically
5. Scalability
– Each request hits the database/authentication service
– No per-request database dependency
6. Credentials exposure
– Username/password sent repeatedly (more risk)
– Credentials stay on client; only token travels
7. Revocation
– Immediate (disable account or change password)
– Difficult without a blacklist or waiting for expiration
8. Mobile-friendly
– Poor (storing credentials securely is hard)
– Better (store token, send with requests)
JWT is better when you need:
1. To reduce Server Load: with Basic auth, the server must validate credentials on every single request, typically hitting a database. With JWT, the server verifies the signature cryptographically without needing to look up user data
2. Stateless Architecture: JWT enables truly stateless servers. You can scale horizontally without sharing session data across servers. Multiple servers can all verify the same JWT independently using the same secret key.
3. Run mobile & microservices: with JWT, mobile apps can store a token locally (not credentials), send it with every API call and tokens can be issued for different services with different permissions. Basic auth requires storing credentials on the device, which is a security risk.
4. Send credentials once: with Basic auth, you’re sending username/password (encoded, not encrypted) on every request. With JWT, credentials are sent only at login; the token is sent afterward.
Basic auth is preferable when:
1. Building a simple internal tool with few users
2. You need immediate revocation (can’t wait for token expiration)
3. You’re developing a small monolithic server without scalability concerns
4. Credentials are rarely used (e.g., API management endpoints)
Working example of JWT authentication over gin:
package main
import (
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
// --- Config
var jwtSecret = []byte("super-secret-key-change-me-in-prod")
// Hardcoded users
var users = map[string]string{
"alberto": "password123",
"admin": "admin",
}
// --- JWT helpers
type Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
func generateToken(username string) (string, error) {
claims := Claims{
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "jwt-demo",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
func parseToken(raw string) (*Claims, error) {
token, err := jwt.ParseWithClaims(raw, &Claims{}, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return jwtSecret, nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, jwt.ErrTokenInvalidClaims
}
return claims, nil
}
// --- Middleware
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "missing or malformed Authorization header",
})
return
}
raw := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := parseToken(raw)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "invalid or expired token",
})
return
}
// Stash the username for downstream handlers.
c.Set("username", claims.Username)
c.Next()
}
}
// --- Handlers
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// POST /auth
func handleAuth(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "username and password are required"})
return
}
expected, ok := users[req.Username]
if !ok || expected != req.Password {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
token, err := generateToken(req.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not generate token"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"expires_in": "1h",
})
}
// GET /secret_data
func handleSecretData(c *gin.Context) {
username, _ := c.Get("username")
c.JSON(http.StatusOK, gin.H{
"message": "Welcome to the secret vault!",
"user": username,
"secrets": []string{
"secret one",
"secret two",
"secret three",
},
})
}
// --- Main
func main() {
r := gin.Default()
r.POST("/auth", handleAuth)
protected := r.Group("/")
protected.Use(authMiddleware())
{
protected.GET("/secret_data", handleSecretData)
}
r.Run(":8080") // listen on http://localhost:8080
}
JWT Auth
– client auth:
curl -X POST http://localhost:8080/auth -H "Content-Type: application/json" -d '{"username":"alberto", "password":"password123"}'
client auth
– client fetch:
TOKEN="<...>"
curl http://localhost:8080/secret_data -H "Authorization: Bearer $TOKEN"
client fetch
AdP